Skip to content

feat(x2a): bulk CSV project creation#2579

Merged
mareklibra merged 2 commits intoredhat-developer:mainfrom
mareklibra:FLPATH-3408.bulkCSVProjectCreation
Mar 23, 2026
Merged

feat(x2a): bulk CSV project creation#2579
mareklibra merged 2 commits intoredhat-developer:mainfrom
mareklibra:FLPATH-3408.bulkCSVProjectCreation

Conversation

@mareklibra
Copy link
Member

Fixes: FLPATH-3408

Users can now create projects in a batch by uploading a CSV file. Documentation is included.

If input errors occur, the same batch can be re-run, automatically skipping any projects that already exist (e.g., from a prior attempt).

A new RepoAuthentication custom widget for the Scaffolder is provided which seamlessly requests tokens of the relevant providers only. This component provides much better user experience compared to the standard RepoUrlPicker in our flow.


x2aBulkProjectCreate.mp4

@rhdh-gh-app
Copy link

rhdh-gh-app bot commented Mar 20, 2026

Changed Packages

Package Name Package Path Changeset Bump Current Version
app workspaces/x2a/packages/app none v0.0.0
@red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-x2a workspaces/x2a/plugins/scaffolder-backend-module-x2a patch v0.1.2
@red-hat-developer-hub/backstage-plugin-x2a-backend workspaces/x2a/plugins/x2a-backend patch v1.0.2
@red-hat-developer-hub/backstage-plugin-x2a-common workspaces/x2a/plugins/x2a-common patch v1.0.2
@red-hat-developer-hub/backstage-plugin-x2a workspaces/x2a/plugins/x2a patch v1.0.2

@rhdh-qodo-merge
Copy link

Review Summary by Qodo

Add bulk CSV project creation with RepoAuthentication scaffolder extension

✨ Enhancement 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• Implements bulk CSV project creation feature allowing users to create multiple projects by
  uploading a CSV file
• Adds new x2a:project:create Scaffolder action supporting both manual and CSV bulk import modes
  with discriminated union schema
• Introduces RepoAuthentication custom Scaffolder field extension that seamlessly requests OAuth
  tokens for each SCM provider found in CSV data
• Implements CSV parser utility with validation for required columns (name, abbreviation,
  sourceRepoUrl, sourceRepoBranch, targetRepoBranch) and optional columns (description,
  ownedByGroup, targetRepoUrl)
• Supports automatic duplicate detection and skipping of already-existing projects, enabling safe
  re-runs of failed batches
• Provides comprehensive test coverage for project creation action (1910 lines), CSV parsing (368
  lines), and RepoAuthentication component (359 lines)
• Includes detailed documentation with CSV format specifications, repository URL examples for
  GitHub/GitLab/Bitbucket, and repeatable import workflow guidance
• Updates project creation template with conditional form fields for manual vs CSV input modes and
  results summary display
• Exports new utilities: parseCsvContent, allProviders, SCAFFOLDER_SECRET_PREFIX, and
  RepoAuthenticationExtension
Diagram
flowchart LR
  CSV["CSV File Upload"] -->|parseCsvContent| Parser["CSV Parser"]
  Parser -->|extract providers| RepoAuth["RepoAuthentication<br/>Extension"]
  RepoAuth -->|request tokens| OAuth["OAuth Tokens"]
  OAuth -->|store as secrets| Secrets["Scaffolder Secrets<br/>OAUTH_TOKEN_*"]
  Secrets -->|augment input| Action["x2a:project:create<br/>Action"]
  Action -->|create & init| Projects["Projects Created<br/>with Tokens"]
  Action -->|detect duplicates| Results["Results:<br/>success/skipped/error"]
Loading

Grey Divider

File Changes

1. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.test.ts 🧪 Tests +1910/-0

Test suite for project creation action

• Comprehensive test suite for the x2a:project:create Scaffolder action with 1910 lines of test
 coverage
• Tests manual project creation flow with various input combinations (tokens, descriptions, group
 ownership)
• Tests CSV bulk import functionality with per-provider token handling and duplicate detection
• Tests token augmentation for different SCM providers (GitHub, GitLab, Bitbucket)
• Tests pagination of existing project names and error scenarios

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.test.ts


2. workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.test.ts 🧪 Tests +368/-0

Test suite for CSV content parser

• Test suite for CSV parsing functionality with 368 lines covering data-URL decoding and validation
• Tests header validation, required/optional column handling, and row-level validation
• Tests CSV edge cases including quoted fields, newlines, escaped quotes, and UTF-8 characters
• Tests optional field defaults and column ordering flexibility

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.test.ts


3. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts ✨ Enhancement +369/-0

Scaffolder action for project creation

• New Scaffolder action x2a:project:create supporting both manual and CSV bulk project creation
• Implements discriminated union schema for manual vs CSV input methods
• Handles token augmentation for different SCM providers and manages existing project name detection
• Supports pagination when fetching existing project names to avoid duplicates
• Outputs projectId, initJobId, nextUrl, and bulk operation counts (successCount,
 errorCount, skippedCount)

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts


View more (27)
4. workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts ✨ Enhancement +124/-0

CSV content parser utility

• New CSV parser utility that decodes base64 data-URL encoded CSV content
• Validates required columns (name, abbreviation, sourceRepoUrl, sourceRepoBranch,
 targetRepoBranch)
• Handles optional columns (description, ownedByGroup, targetRepoUrl) with sensible defaults
• Uses PapaParse library for robust CSV parsing with support for quoted fields and special
 characters

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts


5. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts ✨ Enhancement +115/-0

Project creation and initialization helper

• Helper function that orchestrates project creation and initialization phases
• Creates project via API with normalized repository URLs and optional group ownership
• Triggers init-phase with source/target repository authentication tokens
• Provides detailed logging for debugging and error handling

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts


6. workspaces/x2a/plugins/x2a/src/index.ts ✨ Enhancement +5/-4

Export RepoAuthentication extension

• Exports new RepoAuthenticationExtension for Scaffolder field extension
• Reorganizes exports to use named imports for better clarity

workspaces/x2a/plugins/x2a/src/index.ts


7. workspaces/x2a/plugins/x2a/src/plugin.ts ✨ Enhancement +11/-0

Register RepoAuthentication field extension

• Registers RepoAuthenticationExtension as a Scaffolder field extension
• Provides custom field component for handling SCM provider authentication in templates
• Uses repoAuthenticationValidation for form validation

workspaces/x2a/plugins/x2a/src/plugin.ts


8. workspaces/x2a/plugins/x2a-common/src/csv/index.ts ✨ Enhancement +16/-0

CSV module exports

• New barrel export file for CSV parsing functionality
• Exports parseCsvContent function and CsvProjectRow type

workspaces/x2a/plugins/x2a-common/src/csv/index.ts


9. workspaces/x2a/plugins/x2a-common/src/constants.ts ✨ Enhancement +7/-0

Add scaffolder secret prefix constant

• Adds new constant SCAFFOLDER_SECRET_PREFIX with value 'OAUTH_TOKEN_'
• Used as prefix for SCM provider authentication tokens in Scaffolder secrets

workspaces/x2a/plugins/x2a-common/src/constants.ts


10. workspaces/x2a/plugins/x2a/src/scaffolder/index.ts ✨ Enhancement +15/-0

Scaffolder module exports

• New barrel export file for Scaffolder-related components
• Exports RepoAuthentication component and validation logic

workspaces/x2a/plugins/x2a/src/scaffolder/index.ts


11. workspaces/x2a/plugins/x2a-common/src/scm/providerRegistry.ts ✨ Enhancement +6/-1

Export all SCM providers

• Exports allProviders array as public API for accessing all SCM providers in priority order
• Enables external code to iterate over available providers (GitHub, GitLab, Bitbucket)

workspaces/x2a/plugins/x2a-common/src/scm/providerRegistry.ts


12. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/module.ts Miscellaneous +1/-1

Update import path for project action

• Updates import path from createProject to createProjectAction to match renamed file

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/module.ts


13. workspaces/x2a/plugins/x2a-common/src/scm/index.ts ✨ Enhancement +1/-0

Export allProviders from SCM module

• Exports allProviders from provider registry module

workspaces/x2a/plugins/x2a-common/src/scm/index.ts


14. workspaces/x2a/plugins/x2a-common/src/index.ts ✨ Enhancement +1/-0

Export CSV module from main index

• Adds export of CSV module functionality to main package exports

workspaces/x2a/plugins/x2a-common/src/index.ts


15. workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.test.tsx 🧪 Tests +359/-0

Test suite for RepoAuthentication component

• Test suite for RepoAuthentication field extension component with 359 lines
• Tests rendering, CSV parsing error handling, and authentication flow
• Tests multi-provider authentication and token storage via setSecrets
• Tests error handling with retry functionality and custom CSV field name configuration

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.test.tsx


16. workspaces/x2a/plugins/x2a-common/package.json Dependencies +3/-1

Add papaparse CSV parsing dependency

• Adds papaparse dependency (v5.5.3) for robust CSV parsing
• Adds @types/papaparse dev dependency (v5.5.2) for TypeScript support

workspaces/x2a/plugins/x2a-common/package.json


17. workspaces/x2a/packages/app/package.json Dependencies +1/-0

Add scaffolder-react dependency

• Adds @backstage/plugin-scaffolder-react dependency (v1.20.0) for Scaffolder field extensions

workspaces/x2a/packages/app/package.json


18. workspaces/x2a/.changeset/deep-nails-camp.md 📝 Documentation +5/-0

Changelog entry for CSV bulk creation

• Changelog entry documenting CSV bulk project creation feature as a patch release

workspaces/x2a/.changeset/deep-nails-camp.md


19. workspaces/x2a/templates/conversion-project-template.yaml ✨ Enhancement +154/-58

Add CSV bulk import mode to project creation template

• Added inputMethod parameter with radio button choice between "Manual entry" and "CSV upload"
 modes
• Restructured form fields using JSON schema dependencies to conditionally show manual entry fields
 or CSV upload field based on selected input method
• Added csvContent field with file upload widget accepting .csv files with detailed format
 documentation
• Added repoAuthentication field for CSV mode to handle SCM provider authentication
• Updated action input to pass inputMethod and csvContent parameters
• Added results summary section displaying success, skipped, and error counts from bulk import

workspaces/x2a/templates/conversion-project-template.yaml


20. workspaces/x2a/docs/csv-bulk-import.md 📝 Documentation +126/-0

New CSV bulk project import documentation and guide

• Created comprehensive documentation for CSV bulk import feature including access instructions
• Documented required and optional CSV columns with descriptions and validation rules
• Provided repository URL format examples for GitHub, GitLab, and Bitbucket in both HTTPS and
 RepoUrlPicker styles
• Included repeatable import workflow guidance for handling failures and re-running imports
• Documented RepoAuthentication scaffolder extension with usage examples and registration
 instructions

workspaces/x2a/docs/csv-bulk-import.md


21. workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx ✨ Enhancement +200/-0

New RepoAuthentication scaffolder field extension component

• Implemented RepoAuthentication custom scaffolder field extension component for CSV bulk import
• Parses CSV content to identify distinct SCM providers and requests OAuth authentication for each
• Stores authentication tokens as scaffolder secrets with OAUTH_TOKEN_ prefix
• Provides error handling and retry mechanism for failed authentication attempts
• Exported repoAuthenticationValidation function to block wizard progression until all providers
 are authenticated

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx


22. workspaces/x2a/plugins/x2a/report.api.md Miscellaneous +9/-5

Update API report with RepoAuthentication extension export

• Added export for RepoAuthenticationExtension as a FieldExtensionComponent
• Reordered translation reference entries for consistency (minor formatting changes)

workspaces/x2a/plugins/x2a/report.api.md


23. workspaces/x2a/packages/app/src/App.tsx ✨ Enhancement +11/-1

Register RepoAuthentication extension in scaffolder page

• Imported ScaffolderFieldExtensions from scaffolder-react package
• Imported RepoAuthenticationExtension from x2a plugin
• Restructured /create route to wrap ScaffolderPage with ScaffolderFieldExtensions component
• Added RepoAuthenticationExtension as child of ScaffolderFieldExtensions with note about RHDH
 dynamic plugin replacement

workspaces/x2a/packages/app/src/App.tsx


24. workspaces/x2a/plugins/x2a-common/report.api.md Miscellaneous +12/-0

Export CSV parsing and provider utilities in common package

• Added exports for allProviders constant and CsvProjectRow type
• Added export for parseCsvContent function to parse CSV data URLs
• Added export for SCAFFOLDER_SECRET_PREFIX constant used for storing authentication tokens

workspaces/x2a/plugins/x2a-common/report.api.md


25. workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx Formatting +3/-2

Refactor React context creation import

• Changed React.createContext to createContext import from React for consistency
• Updated ExpandedModulesContext initialization to use imported createContext function

workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx


26. workspaces/x2a/plugins/x2a/package.json Dependencies +1/-0

Add scaffolder-react dependency for field extensions

• Added @backstage/plugin-scaffolder-react dependency version ^1.20.0 to support scaffolder
 field extensions

workspaces/x2a/plugins/x2a/package.json


27. workspaces/x2a/README.md 📝 Documentation +4/-0

Add CSV bulk import documentation reference to README

• Added new section linking to CSV bulk project import documentation
• References detailed guide for CSV file format, examples, and RepoAuthentication extension

workspaces/x2a/README.md


28. workspaces/x2a/.changeset/wise-pianos-shine.md Miscellaneous +7/-0

Add changeset for CSV bulk project creation feature

• Created changeset documenting bulk CSV project creation feature
• Marked as patch version bump for three packages: scaffolder backend module, common package, and
 main x2a plugin

workspaces/x2a/.changeset/wise-pianos-shine.md


29. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts Additional files +0/-993

...

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts


30. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts Additional files +0/-229

...

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts


Grey Divider

Qodo Logo

@rhdh-qodo-merge
Copy link

rhdh-qodo-merge bot commented Mar 20, 2026

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (1) 📐 Spec deviations (0)

Grey Divider


Action required

1. No bulk API endpoint 📎 Requirement gap ✓ Correctness
Description
Bulk creation is implemented only as a Scaffolder action that loops over rows and calls existing
single-project APIs, so there is no standalone backend endpoint to bulk-create projects without the
UI. This prevents external clients/automation from invoking bulk creation directly as required.
Code

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[R269-366]

+        // CSV bulk import — tokens arrive pre-augmented from the RepoAuthentication
+        // frontend extension (which calls provider.augmentToken before storing them
+        // as scaffolder secrets), so no augmentToken call is needed here unlike the
+        // manual flow above.
+        const projectsToCreate = parseCsvContent(ctx.input.csvContent);
+
+        const providerTokens = new Map<ScmProviderName, string>();
+        Object.entries(ctx.secrets ?? {}).forEach(([key, value]) => {
+          if (key.startsWith(SCAFFOLDER_SECRET_PREFIX)) {
+            const providerName = key.replace(SCAFFOLDER_SECRET_PREFIX, '');
+            if (
+              allProviders.some(
+                (provider: ScmProvider) => provider.name === providerName,
+              )
+            ) {
+              providerTokens.set(providerName as ScmProviderName, value);
+            }
+          }
+        });
+
+        if (providerTokens.size === 0) {
+          throw new Error(
+            'At least one SCM provider authentication token is required for CSV import',
+          );
+        }
+
+        let successCount = 0;
+        let errorCount = 0;
+        let skippedCount = 0;
+
+        // Create projects sequentially to avoid overwhelming the API
+        for (const row of projectsToCreate) {
+          if (existingProjectNames.has(row.name)) {
+            ctx.logger.warn(
+              `Skipping project "${row.name}": a project with this name already exists. ` +
+                `To import it anyway, either change the project name or delete the existing project.`,
+            );
+            skippedCount++;
+            continue;
+          }
+
+          const sourceProvider = resolveScmProvider(
+            row.sourceRepoUrl,
+            hostProviderMap,
+          );
+          const sourceRepoToken = providerTokens.get(sourceProvider.name);
+          if (!sourceRepoToken) {
+            ctx.logger.error(
+              `Skipping project "${row.name}": no ${sourceProvider.name} authentication token provided (source: ${row.sourceRepoUrl})`,
+            );
+            errorCount++;
+            continue;
+          }
+
+          const targetUrl = row.targetRepoUrl ?? row.sourceRepoUrl;
+          const targetProvider = resolveScmProvider(targetUrl, hostProviderMap);
+          const targetRepoToken = providerTokens.get(targetProvider.name);
+          if (!targetRepoToken) {
+            ctx.logger.error(
+              `Skipping project "${row.name}": no ${targetProvider.name} authentication token provided (target: ${targetUrl})`,
+            );
+            errorCount++;
+            continue;
+          }
+
+          try {
+            await createAndInitProject({
+              api,
+              row,
+              sourceRepoToken,
+              targetRepoToken,
+              userPrompt: ctx.input.userPrompt,
+              backstageToken: token,
+              hostProviderMap,
+              logger: ctx.logger,
+            });
+            existingProjectNames.add(row.name);
+            successCount++;
+          } catch {
+            errorCount++;
+          }
+        }
+
+        ctx.logger.info(
+          `Bulk CSV import complete: ${successCount} succeeded, ${errorCount} failed, ${skippedCount} skipped out of ${projectsToCreate.length} project(s)`,
+        );
+
+        ctx.output('successCount', successCount);
+        ctx.output('errorCount', errorCount);
+        ctx.output('skippedCount', skippedCount);
+        ctx.output('nextUrl', '/x2a/projects');
+
+        if (errorCount > 0) {
+          throw new Error(
+            `CSV import completed with errors: ${successCount} succeeded, ${errorCount} failed, ${skippedCount} skipped out of ${projectsToCreate.length} project(s)`,
+          );
+        }
+      }
Evidence
PR Compliance ID 2 requires a new bulk project creation API endpoint callable independently of the
UI. The added x2a:project:create Scaffolder action performs CSV bulk creation by iterating rows
and calling createAndInitProject for each, which in turn calls the existing per-project endpoints
projectsPost and projectsProjectIdRunPost, rather than exposing a dedicated bulk-create API
endpoint.

Provide an API endpoint for bulk project creation callable without the UI
workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[269-366]
workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[45-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR adds CSV bulk creation via a Scaffolder action, but does not add a standalone backend API endpoint for bulk project creation callable without the UI.

## Issue Context
The current implementation loops over CSV rows and calls existing single-project endpoints (`projectsPost` and `projectsProjectIdRunPost`). Compliance requires a dedicated API endpoint that external clients can call directly to create multiple projects in one operation.

## Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[269-366]
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[45-92]
- workspaces/x2a/plugins/x2a-backend/src/router/projects.ts[1-400]
- workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/router.ts[36-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Node Buffer used in frontend 🐞 Bug ✓ Correctness
Description
parseCsvContent decodes the base64 payload using global Buffer, but RepoAuthentication calls
parseCsvContent in the browser; if Buffer isn’t provided by the frontend bundle, CSV parsing will
crash and the CSV import wizard can’t proceed.
Code

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[R57-65]

+export function parseCsvContent(dataUrl: string): CsvProjectRow[] {
+  const base64Match = dataUrl.match(/^data:(?:[^;]*;)*base64,(.*)$/);
+  if (!base64Match) {
+    throw new Error('Invalid CSV content: expected a base64-encoded data-URL');
+  }
+
+  const csvText = Buffer.from(base64Match[1], 'base64').toString('utf-8');
+
+  const result = Papa.parse<Record<string, string>>(csvText, {
Evidence
The shared CSV parser decodes via Buffer (Node API) and is directly invoked from the frontend field
extension during CSV import, but the shared package does not declare any browser Buffer/polyfill
dependency.

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[57-66]
workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[70-83]
workspaces/x2a/plugins/x2a-common/package.json[51-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`parseCsvContent` uses `Buffer.from(..., &#x27;base64&#x27;)` to decode the data-URL payload. This assumes a Node/global `Buffer`, but `parseCsvContent` is also called from the frontend (`RepoAuthentication`), where `Buffer` is not a standard browser global.

### Issue Context
`parseCsvContent` is in `x2a-common` (shared) and is used both server-side and client-side. It should use a decoding approach that works in both environments, or explicitly import a polyfill.

### Fix Focus Areas
- workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[57-66]
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[70-83]

### Suggested implementation direction
- Replace `Buffer.from(...).toString(&#x27;utf-8&#x27;)` with a small helper:
 - If `globalThis.Buffer` exists, use it.
 - Else use `atob` + `TextDecoder(&#x27;utf-8&#x27;)` to decode base64 into UTF-8.
- Keep the rest of the parsing logic unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Auth not rerun on CSV change 🐞 Bug ✓ Correctness
Description
RepoAuthentication stops re-authentication permanently after the first success (isDone), so
uploading a different CSV later in the wizard won’t trigger authentication for newly introduced SCM
providers and the backend CSV import will fail due to missing provider tokens.
Code

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[R63-129]

+  useEffect(() => {
+    if (!csvContent || suppressDialog || isDone) {
+      return;
+    }
+
+    setError(undefined);
+
+    let projectsToCreate;
+    try {
+      projectsToCreate = parseCsvContent(csvContent);
+    } catch (e) {
+      setError(e instanceof Error ? e.message : 'Unknown error');
+      return;
+    }
+
+    const allTargetProviders: ScmProvider[] = projectsToCreate.map(project =>
+      resolveScmProvider(project.targetRepoUrl, hostProviderMap),
+    );
+    const allSourceProviders: ScmProvider[] = projectsToCreate.map(project =>
+      resolveScmProvider(project.sourceRepoUrl, hostProviderMap),
+    );
+    const distinctTargetProviders = allTargetProviders.filter(
+      (p, i, arr) => arr.findIndex(q => q.name === p.name) === i,
+    );
+    const distinctSourceProviders = allSourceProviders.filter(
+      (p, i, arr) =>
+        arr.findIndex(q => q.name === p.name) === i &&
+        !distinctTargetProviders.some(t => t.name === p.name),
+    );
+    const allDistinctProviders = [
+      ...distinctTargetProviders,
+      ...distinctSourceProviders,
+    ];
+
+    const doAuthAsync = async () => {
+      const providerTokens = new Map<string, string>();
+
+      const authenticateProviders = (
+        providers: ScmProvider[],
+        readOnly: boolean,
+      ) =>
+        providers.map(provider =>
+          repoAuthentication
+            .authenticate([provider.getAuthTokenDescriptor(readOnly)])
+            .then(tokens => {
+              providerTokens.set(
+                `${SCAFFOLDER_SECRET_PREFIX}${provider.name}`,
+                tokens[0].token,
+              );
+            })
+            .catch(e => {
+              setError(e instanceof Error ? e.message : 'Unknown error');
+              setSuppressDialog(true);
+            }),
+        );
+
+      await Promise.all([
+        ...authenticateProviders(distinctTargetProviders, false),
+        ...authenticateProviders(distinctSourceProviders, true),
+      ]);
+
+      if (providerTokens.size === allDistinctProviders.length) {
+        onChange('authenticated');
+        setDone(true);
+      } else {
+        onChange(undefined);
+      }
Evidence
The effect exits early when isDone is true, and isDone is set to true after the first successful
authentication. There is no reset path when csvContent changes, so subsequent CSV changes won’t
re-run auth/provider detection.

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[63-66]
workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[124-129]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`RepoAuthentication` sets `isDone=true` after a successful auth, and the main `useEffect` returns early if `isDone` is true. This prevents re-auth when the user replaces/edits the uploaded CSV (which can change the set of required SCM providers).

### Issue Context
The feature is explicitly designed to support re-running imports and correcting inputs; the UI must also handle users changing the CSV in the wizard.

### Fix Focus Areas
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[63-66]
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[124-129]

### Suggested implementation direction
- Track the last processed `csvContent` (or a derived provider-set key) in a ref/state.
- When `csvContent` changes, reset `isDone` (and optionally `suppressDialog`/`error`) so authentication runs again for the new provider set.
- Ensure `onChange(undefined)` is emitted when switching CSVs until auth succeeds for the new CSV.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Manual output counts undefined 🐞 Bug ✓ Correctness
Description
In manual mode the action only outputs successCount, but the template always renders skippedCount
and errorCount, resulting in user-visible undefined values (and a schema/output contract mismatch).
Code

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[R263-268]

+        ctx.output('projectId', result.projectId);
+        ctx.output('initJobId', result.initJobId);
+        ctx.output('successCount', 1);
+        // no need for skippedCount and errorCount
+        ctx.output('nextUrl', `/x2a/projects/${result.projectId}`);
+      } else {
Evidence
The manual branch intentionally omits skippedCount and errorCount, while the template output
block always prints both values. This will display as empty/undefined for manual runs.

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[263-268]
workspaces/x2a/templates/conversion-project-template.yaml[228-233]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Manual project creation outputs `successCount` but not `skippedCount`/`errorCount`, while the template output always references `skippedCount` and `errorCount`. This leads to confusing output for users (e.g., “Skipped: undefined”).

### Issue Context
The template’s `output.text` block is shared for both manual and CSV modes.

### Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[263-268]
- workspaces/x2a/templates/conversion-project-template.yaml[228-233]

### Suggested implementation direction
- In the manual branch, add:
 - `ctx.output(&#x27;skippedCount&#x27;, 0)`
 - `ctx.output(&#x27;errorCount&#x27;, 0)`
- (Optional) Consider making `projectId`/`initJobId` outputs optional in the action schema if they are not emitted in CSV mode, but the immediate user-facing issue is the missing counts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Duplicate-check ignores non-OK 🐞 Bug ⛯ Reliability
Description
fetchExistingProjectNames doesn’t check response.ok, so if /projects returns a non-2xx response the
code will treat the error JSON as data and likely return an empty set, silently disabling duplicate
detection and “skip existing” CSV reruns.
Code

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[R43-52]

+  for (;;) {
+    const response = await api.projectsGet(
+      { query: { page, pageSize } },
+      { token },
+    );
+    const data = await response.json();
+
+    if (!data.items || data.items.length === 0) {
+      break;
+    }
Evidence
DefaultApiClient.projectsGet returns the raw fetch Response regardless of status;
fetchExistingProjectNames unconditionally calls response.json() and then stops when data.items is
missing, producing an incomplete/empty name set without surfacing the failure.

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[43-52]
workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/apis/Api.client.ts[214-234]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`fetchExistingProjectNames` assumes `/projects` always returns a valid success payload. On non-2xx responses, it still calls `response.json()` and then exits when `data.items` is missing, silently returning an empty set. This breaks:
- manual duplicate-name pre-check
- CSV ‘skip existing’ behavior for re-runs

### Issue Context
`DefaultApiClient.projectsGet` returns the raw `fetch` response and does not throw on non-OK.

### Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[35-64]

### Suggested implementation direction
- Add `if (!response.ok)` handling:
 - Parse the error body (best-effort) and throw a clear error like: “Unable to list existing projects (status X): ...”.
 - Alternatively, log a warning and proceed, but do so explicitly so operators/users understand that duplicate-skipping is disabled.
- Consider adding tests for non-OK responses to ensure behavior is intentional and visible.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Empty ownedByGroup sent 🐞 Bug ✓ Correctness
Description
createAndInitProject uses row.ownedByGroup?.trim() ?? undefined, which turns whitespace-only
values into an empty string and sends that to the API instead of omitting ownedByGroup.
Code

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[R45-52]

+  const body: ProjectsPost['body'] = {
+    name: row.name,
+    description: row.description ?? '',
+    abbreviation: row.abbreviation,
+    ownedByGroup: row.ownedByGroup?.trim() ?? undefined,
+    sourceRepoUrl: normalizeRepoUrl(row.sourceRepoUrl),
+    targetRepoUrl: normalizeRepoUrl(row.targetRepoUrl ?? row.sourceRepoUrl),
+    sourceRepoBranch: row.sourceRepoBranch,
Evidence
?? undefined does not convert empty strings to undefined. The CSV parser already normalizes blank
ownedByGroup to undefined via || undefined, indicating the intended semantics are to omit blanks;
createAndInitProject should match that behavior.

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[45-52]
workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[110-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ownedByGroup` is currently computed with `row.ownedByGroup?.trim() ?? undefined`, which will keep `&#x27;&#x27;` (empty string) instead of omitting it. This can result in sending an empty `ownedByGroup` to the backend.

### Issue Context
The CSV parsing path already uses `|| undefined` for `ownedByGroup`, so the create call should align.

### Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[45-52]

### Suggested implementation direction
- Change to `ownedByGroup: row.ownedByGroup?.trim() || undefined`.
- (Optional) Add/extend a unit test to cover whitespace-only `ownedByGroup` and assert it is omitted from the request body.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

7. CSV header help text wrong 🐞 Bug ⚙ Maintainability
Description
The template’s CSV upload description lists targetRepoBranch as both required and optional,
contradicting the actual parser requirements and confusing users about valid CSV format.
Code

workspaces/x2a/templates/conversion-project-template.yaml[R92-95]

+                      The CSV file must contain the following headers: name, abbreviation, sourceRepoUrl, sourceRepoBranch, targetRepoBranch.
+
+                      Optional headers: description, ownedByGroup, targetRepoUrl, targetRepoBranch.
+
Evidence
The template help text contradicts itself, while the parser clearly defines targetRepoBranch as
required and does not include it in optional headers.

workspaces/x2a/templates/conversion-project-template.yaml[92-95]
workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[27-39]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The CSV upload help text states `targetRepoBranch` is required and also lists it under optional headers. This is inconsistent with the parser and can cause users to build invalid CSVs.

### Issue Context
`parseCsvContent` treats `targetRepoBranch` as required.

### Fix Focus Areas
- workspaces/x2a/templates/conversion-project-template.yaml[92-95]

### Suggested implementation direction
- Remove `targetRepoBranch` from the optional headers list in the description so it matches the parser’s required/optional header sets.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@mareklibra mareklibra changed the title Flpath 3408.bulk csv project creation feat(x2a): bulk CSV project creation Mar 20, 2026
@mareklibra mareklibra marked this pull request as draft March 20, 2026 08:22
@mareklibra mareklibra force-pushed the FLPATH-3408.bulkCSVProjectCreation branch from 33a2ee5 to 7d98a81 Compare March 20, 2026 08:23
Comment on lines +269 to +366
// CSV bulk import — tokens arrive pre-augmented from the RepoAuthentication
// frontend extension (which calls provider.augmentToken before storing them
// as scaffolder secrets), so no augmentToken call is needed here unlike the
// manual flow above.
const projectsToCreate = parseCsvContent(ctx.input.csvContent);

const providerTokens = new Map<ScmProviderName, string>();
Object.entries(ctx.secrets ?? {}).forEach(([key, value]) => {
if (key.startsWith(SCAFFOLDER_SECRET_PREFIX)) {
const providerName = key.replace(SCAFFOLDER_SECRET_PREFIX, '');
if (
allProviders.some(
(provider: ScmProvider) => provider.name === providerName,
)
) {
providerTokens.set(providerName as ScmProviderName, value);
}
}
});

if (providerTokens.size === 0) {
throw new Error(
'At least one SCM provider authentication token is required for CSV import',
);
}

let successCount = 0;
let errorCount = 0;
let skippedCount = 0;

// Create projects sequentially to avoid overwhelming the API
for (const row of projectsToCreate) {
if (existingProjectNames.has(row.name)) {
ctx.logger.warn(
`Skipping project "${row.name}": a project with this name already exists. ` +
`To import it anyway, either change the project name or delete the existing project.`,
);
skippedCount++;
continue;
}

const sourceProvider = resolveScmProvider(
row.sourceRepoUrl,
hostProviderMap,
);
const sourceRepoToken = providerTokens.get(sourceProvider.name);
if (!sourceRepoToken) {
ctx.logger.error(
`Skipping project "${row.name}": no ${sourceProvider.name} authentication token provided (source: ${row.sourceRepoUrl})`,
);
errorCount++;
continue;
}

const targetUrl = row.targetRepoUrl ?? row.sourceRepoUrl;
const targetProvider = resolveScmProvider(targetUrl, hostProviderMap);
const targetRepoToken = providerTokens.get(targetProvider.name);
if (!targetRepoToken) {
ctx.logger.error(
`Skipping project "${row.name}": no ${targetProvider.name} authentication token provided (target: ${targetUrl})`,
);
errorCount++;
continue;
}

try {
await createAndInitProject({
api,
row,
sourceRepoToken,
targetRepoToken,
userPrompt: ctx.input.userPrompt,
backstageToken: token,
hostProviderMap,
logger: ctx.logger,
});
existingProjectNames.add(row.name);
successCount++;
} catch {
errorCount++;
}
}

ctx.logger.info(
`Bulk CSV import complete: ${successCount} succeeded, ${errorCount} failed, ${skippedCount} skipped out of ${projectsToCreate.length} project(s)`,
);

ctx.output('successCount', successCount);
ctx.output('errorCount', errorCount);
ctx.output('skippedCount', skippedCount);
ctx.output('nextUrl', '/x2a/projects');

if (errorCount > 0) {
throw new Error(
`CSV import completed with errors: ${successCount} succeeded, ${errorCount} failed, ${skippedCount} skipped out of ${projectsToCreate.length} project(s)`,
);
}
}

Choose a reason for hiding this comment

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

Action required

1. No bulk api endpoint 📎 Requirement gap ✓ Correctness

Bulk creation is implemented only as a Scaffolder action that loops over rows and calls existing
single-project APIs, so there is no standalone backend endpoint to bulk-create projects without the
UI. This prevents external clients/automation from invoking bulk creation directly as required.
Agent Prompt
## Issue description
The PR adds CSV bulk creation via a Scaffolder action, but does not add a standalone backend API endpoint for bulk project creation callable without the UI.

## Issue Context
The current implementation loops over CSV rows and calls existing single-project endpoints (`projectsPost` and `projectsProjectIdRunPost`). Compliance requires a dedicated API endpoint that external clients can call directly to create multiple projects in one operation.

## Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[269-366]
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[45-92]
- workspaces/x2a/plugins/x2a-backend/src/router/projects.ts[1-400]
- workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/router.ts[36-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Member Author

Choose a reason for hiding this comment

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

We do not want to implement such endpoint, it would be unnecessary complication.
When the API is used without the UI, the caller can implement such loop on its own and so better deal with authentication.

Choose a reason for hiding this comment

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

Question:

We do not want to implement such endpoint, it would be unnecessary complication. When the API is used without the UI, the caller can implement such loop on its own and so better deal with authentication.

Answer:

To stop Qodo Merge from repeatedly suggesting “add a new backend bulk endpoint” (when your team explicitly doesn’t want new endpoints), add a repo policy as extra instructions for the /improve tool:

[pr_code_suggestions]
extra_instructions="""\
- Do NOT suggest adding new backend API endpoints (including “bulk” endpoints) unless the PR/ticket explicitly requires it.
- If a change can be achieved by client-side orchestration (looping / batching) over existing single-item endpoints, treat that as acceptable and do not flag “missing bulk endpoint” as a requirement gap.
- Avoid architecture-expanding recommendations (new routes, new OpenAPI surfaces) unless they are explicitly requested; prefer minimal, incremental changes within the current design.
"""

If you want to quickly verify this is being applied, run:

/improve --config.output_relevant_configurations=true

Relevant Sources:

Comment on lines +57 to +65
export function parseCsvContent(dataUrl: string): CsvProjectRow[] {
const base64Match = dataUrl.match(/^data:(?:[^;]*;)*base64,(.*)$/);
if (!base64Match) {
throw new Error('Invalid CSV content: expected a base64-encoded data-URL');
}

const csvText = Buffer.from(base64Match[1], 'base64').toString('utf-8');

const result = Papa.parse<Record<string, string>>(csvText, {

Choose a reason for hiding this comment

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

Action required

2. Node buffer used in frontend 🐞 Bug ✓ Correctness

parseCsvContent decodes the base64 payload using global Buffer, but RepoAuthentication calls
parseCsvContent in the browser; if Buffer isn’t provided by the frontend bundle, CSV parsing will
crash and the CSV import wizard can’t proceed.
Agent Prompt
### Issue description
`parseCsvContent` uses `Buffer.from(..., 'base64')` to decode the data-URL payload. This assumes a Node/global `Buffer`, but `parseCsvContent` is also called from the frontend (`RepoAuthentication`), where `Buffer` is not a standard browser global.

### Issue Context
`parseCsvContent` is in `x2a-common` (shared) and is used both server-side and client-side. It should use a decoding approach that works in both environments, or explicitly import a polyfill.

### Fix Focus Areas
- workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[57-66]
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[70-83]

### Suggested implementation direction
- Replace `Buffer.from(...).toString('utf-8')` with a small helper:
  - If `globalThis.Buffer` exists, use it.
  - Else use `atob` + `TextDecoder('utf-8')` to decode base64 into UTF-8.
- Keep the rest of the parsing logic unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +63 to +129
useEffect(() => {
if (!csvContent || suppressDialog || isDone) {
return;
}

setError(undefined);

let projectsToCreate;
try {
projectsToCreate = parseCsvContent(csvContent);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
return;
}

const allTargetProviders: ScmProvider[] = projectsToCreate.map(project =>
resolveScmProvider(project.targetRepoUrl, hostProviderMap),
);
const allSourceProviders: ScmProvider[] = projectsToCreate.map(project =>
resolveScmProvider(project.sourceRepoUrl, hostProviderMap),
);
const distinctTargetProviders = allTargetProviders.filter(
(p, i, arr) => arr.findIndex(q => q.name === p.name) === i,
);
const distinctSourceProviders = allSourceProviders.filter(
(p, i, arr) =>
arr.findIndex(q => q.name === p.name) === i &&
!distinctTargetProviders.some(t => t.name === p.name),
);
const allDistinctProviders = [
...distinctTargetProviders,
...distinctSourceProviders,
];

const doAuthAsync = async () => {
const providerTokens = new Map<string, string>();

const authenticateProviders = (
providers: ScmProvider[],
readOnly: boolean,
) =>
providers.map(provider =>
repoAuthentication
.authenticate([provider.getAuthTokenDescriptor(readOnly)])
.then(tokens => {
providerTokens.set(
`${SCAFFOLDER_SECRET_PREFIX}${provider.name}`,
tokens[0].token,
);
})
.catch(e => {
setError(e instanceof Error ? e.message : 'Unknown error');
setSuppressDialog(true);
}),
);

await Promise.all([
...authenticateProviders(distinctTargetProviders, false),
...authenticateProviders(distinctSourceProviders, true),
]);

if (providerTokens.size === allDistinctProviders.length) {
onChange('authenticated');
setDone(true);
} else {
onChange(undefined);
}

Choose a reason for hiding this comment

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

Action required

3. Auth not rerun on csv change 🐞 Bug ✓ Correctness

RepoAuthentication stops re-authentication permanently after the first success (isDone), so
uploading a different CSV later in the wizard won’t trigger authentication for newly introduced SCM
providers and the backend CSV import will fail due to missing provider tokens.
Agent Prompt
### Issue description
`RepoAuthentication` sets `isDone=true` after a successful auth, and the main `useEffect` returns early if `isDone` is true. This prevents re-auth when the user replaces/edits the uploaded CSV (which can change the set of required SCM providers).

### Issue Context
The feature is explicitly designed to support re-running imports and correcting inputs; the UI must also handle users changing the CSV in the wizard.

### Fix Focus Areas
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[63-66]
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[124-129]

### Suggested implementation direction
- Track the last processed `csvContent` (or a derived provider-set key) in a ref/state.
- When `csvContent` changes, reset `isDone` (and optionally `suppressDialog`/`error`) so authentication runs again for the new provider set.
- Ensure `onChange(undefined)` is emitted when switching CSVs until auth succeeds for the new CSV.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +263 to +268
ctx.output('projectId', result.projectId);
ctx.output('initJobId', result.initJobId);
ctx.output('successCount', 1);
// no need for skippedCount and errorCount
ctx.output('nextUrl', `/x2a/projects/${result.projectId}`);
} else {

Choose a reason for hiding this comment

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

Action required

4. Manual output counts undefined 🐞 Bug ✓ Correctness

In manual mode the action only outputs successCount, but the template always renders skippedCount
and errorCount, resulting in user-visible undefined values (and a schema/output contract mismatch).
Agent Prompt
### Issue description
Manual project creation outputs `successCount` but not `skippedCount`/`errorCount`, while the template output always references `skippedCount` and `errorCount`. This leads to confusing output for users (e.g., “Skipped: undefined”).

### Issue Context
The template’s `output.text` block is shared for both manual and CSV modes.

### Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[263-268]
- workspaces/x2a/templates/conversion-project-template.yaml[228-233]

### Suggested implementation direction
- In the manual branch, add:
  - `ctx.output('skippedCount', 0)`
  - `ctx.output('errorCount', 0)`
- (Optional) Consider making `projectId`/`initJobId` outputs optional in the action schema if they are not emitted in CSV mode, but the immediate user-facing issue is the missing counts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +43 to +52
for (;;) {
const response = await api.projectsGet(
{ query: { page, pageSize } },
{ token },
);
const data = await response.json();

if (!data.items || data.items.length === 0) {
break;
}

Choose a reason for hiding this comment

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

Action required

5. Duplicate-check ignores non-ok 🐞 Bug ⛯ Reliability

fetchExistingProjectNames doesn’t check response.ok, so if /projects returns a non-2xx response the
code will treat the error JSON as data and likely return an empty set, silently disabling duplicate
detection and “skip existing” CSV reruns.
Agent Prompt
### Issue description
`fetchExistingProjectNames` assumes `/projects` always returns a valid success payload. On non-2xx responses, it still calls `response.json()` and then exits when `data.items` is missing, silently returning an empty set. This breaks:
- manual duplicate-name pre-check
- CSV ‘skip existing’ behavior for re-runs

### Issue Context
`DefaultApiClient.projectsGet` returns the raw `fetch` response and does not throw on non-OK.

### Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[35-64]

### Suggested implementation direction
- Add `if (!response.ok)` handling:
  - Parse the error body (best-effort) and throw a clear error like: “Unable to list existing projects (status X): ...”.
  - Alternatively, log a warning and proceed, but do so explicitly so operators/users understand that duplicate-skipping is disabled.
- Consider adding tests for non-OK responses to ensure behavior is intentional and visible.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@mareklibra mareklibra force-pushed the FLPATH-3408.bulkCSVProjectCreation branch from 7d98a81 to b5ebcf6 Compare March 21, 2026 08:31
@mareklibra mareklibra marked this pull request as ready for review March 21, 2026 08:36
@rhdh-qodo-merge
Copy link

Review Summary by Qodo

Bulk CSV project creation with RepoAuthentication Scaffolder extension

✨ Enhancement 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• Implements bulk CSV project creation feature allowing users to upload a CSV file to create
  multiple projects in batch
• Introduces RepoAuthentication custom Scaffolder field extension that seamlessly requests OAuth
  tokens for relevant SCM providers (GitHub, GitLab, Bitbucket)
• Adds x2a:project:create Scaffolder action supporting both manual and CSV bulk import modes with
  pagination-based duplicate detection
• Implements CSV parser utility (parseCsvContent) with validation for required columns (name,
  abbreviation, sourceRepoUrl, sourceRepoBranch, targetRepoBranch) and optional fields
• Provides automatic retry capability - same batch can be re-run, automatically skipping projects
  that already exist from prior attempts
• Includes comprehensive test coverage (2100+ lines for action, 400+ lines for CSV parser, 466 lines
  for RepoAuthentication component)
• Adds detailed documentation for CSV file format, repeatable import workflow, and repository URL
  examples for different providers
• Updates conversion project template with input method toggle, conditional form fields, and results
  summary output
• Registers RepoAuthentication extension in Scaffolder with proper validation and error handling
Diagram
flowchart LR
  CSV["CSV File Upload"]
  Parser["parseCsvContent<br/>Parser"]
  RepoAuth["RepoAuthentication<br/>Extension"]
  Tokens["OAuth Tokens<br/>per Provider"]
  Action["x2a:project:create<br/>Action"]
  Projects["Projects Created<br/>with Duplicate Skip"]
  
  CSV --> Parser
  Parser --> RepoAuth
  RepoAuth --> Tokens
  Tokens --> Action
  Action --> Projects
Loading

Grey Divider

File Changes

1. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.test.ts 🧪 Tests +2142/-0

Comprehensive test suite for project creation action

• Comprehensive test suite for the x2a:project:create Scaffolder action with 2100+ lines of test
 coverage
• Tests manual project creation, CSV bulk import, token augmentation for GitHub/GitLab/Bitbucket,
 and duplicate detection
• Validates pagination of existing projects, error handling, and per-provider token extraction from
 secrets
• Includes contract tests ensuring compatibility between RepoAuthentication widget and the action

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.test.ts


2. workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.test.ts 🧪 Tests +410/-0

CSV content parsing test suite

• Test suite for CSV parsing functionality with 400+ lines covering data-URL decoding and CSV
 validation
• Tests header validation, required/optional field handling, row validation, and edge cases (quoted
 fields, UTF-8, CRLF)
• Validates fallback to atob when Buffer is unavailable and proper handling of column ordering

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.test.ts


3. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts ✨ Enhancement +452/-0

Project creation Scaffolder action with CSV bulk import

• New Scaffolder action x2a:project:create supporting both manual and CSV bulk project creation
 modes
• Implements pagination-based duplicate project name detection and per-provider token extraction
 from secrets
• Handles token augmentation for different SCM providers (GitHub, GitLab, Bitbucket) and manages
 both shared and separate source/target repositories
• Provides detailed error reporting and logging for bulk CSV imports with success/error/skipped
 counts

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts


View more (26)
4. workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts ✨ Enhancement +140/-0

CSV content parser for bulk project import

• New CSV parser utility using PapaParse for decoding base64 data-URLs and parsing project rows
• Validates required columns (name, abbreviation, sourceRepoUrl, sourceRepoBranch,
 targetRepoBranch) and optional fields
• Handles UTF-8 decoding with fallback to atob for browser environments and normalizes field
 values (trimming, defaults)

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts


5. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts ✨ Enhancement +119/-0

Project creation and initialization helper

• Helper function extracted to create a project and trigger its initialization phase
• Handles API calls to create project and run init-phase with repository authentication tokens
• Provides detailed logging for debugging and error reporting during project creation workflow

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts


6. workspaces/x2a/plugins/x2a/src/index.ts ✨ Enhancement +5/-4

Export RepoAuthentication Scaffolder extension

• Exports new RepoAuthenticationExtension for Scaffolder field extension
• Reorganizes translation hook exports for consistency

workspaces/x2a/plugins/x2a/src/index.ts


7. workspaces/x2a/plugins/x2a/src/plugin.ts ✨ Enhancement +11/-0

Register RepoAuthentication Scaffolder field extension

• Registers RepoAuthentication as a Scaffolder field extension with validation
• Imports and provides the custom widget for seamless SCM provider token collection in templates

workspaces/x2a/plugins/x2a/src/plugin.ts


8. workspaces/x2a/plugins/x2a-common/src/csv/index.ts ✨ Enhancement +16/-0

CSV module barrel exports

• New barrel export file for CSV parsing functionality
• Exports parseCsvContent function and CsvProjectRow type

workspaces/x2a/plugins/x2a-common/src/csv/index.ts


9. workspaces/x2a/plugins/x2a-common/src/constants.ts ✨ Enhancement +7/-0

Add scaffolder secret prefix constant

• Adds SCAFFOLDER_SECRET_PREFIX constant set to 'OAUTH_TOKEN_' for SCM provider token secret
 keys

workspaces/x2a/plugins/x2a-common/src/constants.ts


10. workspaces/x2a/plugins/x2a/src/scaffolder/index.ts ✨ Enhancement +15/-0

Scaffolder module barrel exports

• New barrel export file for Scaffolder-related components
• Exports RepoAuthentication component and validation logic

workspaces/x2a/plugins/x2a/src/scaffolder/index.ts


11. workspaces/x2a/plugins/x2a-common/src/scm/providerRegistry.ts ✨ Enhancement +6/-1

Export all SCM providers array

• Exports allProviders array as public API for accessing all SCM providers in priority order

workspaces/x2a/plugins/x2a-common/src/scm/providerRegistry.ts


12. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/module.ts Miscellaneous +1/-1

Update import path for project creation action

• Updates import path from createProject to createProjectAction to match renamed file

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/module.ts


13. workspaces/x2a/plugins/x2a-common/src/scm/index.ts ✨ Enhancement +1/-0

Export all providers from SCM module

• Exports allProviders from provider registry for public API access

workspaces/x2a/plugins/x2a-common/src/scm/index.ts


14. workspaces/x2a/plugins/x2a-common/src/index.ts ✨ Enhancement +1/-0

Export CSV module from common package

• Adds barrel export for CSV module functionality

workspaces/x2a/plugins/x2a-common/src/index.ts


15. workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.test.tsx 🧪 Tests +466/-0

RepoAuthentication component test suite

• Comprehensive test suite for the RepoAuthentication component with 466 lines of test coverage
• Tests rendering, CSV parsing errors, authentication flow, error handling, and retry logic
• Validates the repoAuthenticationValidation function for form submission blocking
• Mocks scaffolder secrets, SCM host map, and repository authentication hooks

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.test.tsx


16. workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx ✨ Enhancement +210/-0

RepoAuthentication scaffolder field extension implementation

• New RepoAuthentication custom scaffolder field extension for bulk CSV project creation
• Parses CSV content to identify distinct SCM providers and requests OAuth tokens for each
• Stores authenticated tokens as scaffolder secrets with OAUTH_TOKEN_ prefix
• Provides repoAuthenticationValidation function to block wizard progression until authentication
 completes

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx


17. workspaces/x2a/templates/conversion-project-template.yaml ✨ Enhancement +152/-58

Template refactoring for CSV bulk import support

• Adds inputMethod radio field to toggle between manual entry and CSV upload modes
• Restructures form parameters using JSON schema dependencies to conditionally show/hide fields
 based on input method
• Adds CSV file upload field with detailed documentation on format, headers, and repeatable import
 workflow
• Adds repoAuthentication field for CSV mode to request SCM provider credentials
• Updates template action input to pass inputMethod and csvContent parameters
• Adds results summary output showing success, skip, and error counts

workspaces/x2a/templates/conversion-project-template.yaml


18. workspaces/x2a/docs/csv-bulk-import.md 📝 Documentation +126/-0

CSV bulk import feature documentation

• New comprehensive documentation for CSV bulk project import feature
• Includes CSV file format specification with required and optional columns
• Documents repeatable import workflow for handling failures and retries
• Provides repository URL format examples for GitHub, GitLab, and Bitbucket
• Explains RepoAuthentication scaffolder extension functionality and registration
• Includes practical example CSV with mixed provider URLs

workspaces/x2a/docs/csv-bulk-import.md


19. workspaces/x2a/plugins/x2a/report.api.md Miscellaneous +11/-7

API report updates for RepoAuthentication extension

• Exports new RepoAuthenticationExtension field extension component
• Updates import paths for TranslationRef and TranslationResource from
 @backstage/frontend-plugin-api to @backstage/core-plugin-api/alpha
• Reorders translation reference keys (cosmetic changes to alphabetical ordering)

workspaces/x2a/plugins/x2a/report.api.md


20. workspaces/x2a/packages/app/src/App.tsx ✨ Enhancement +11/-1

App scaffolder field extensions registration

• Imports ScaffolderFieldExtensions from @backstage/plugin-scaffolder-react
• Imports RepoAuthenticationExtension from x2a plugin
• Wraps /create route with ScaffolderFieldExtensions component containing
 RepoAuthenticationExtension
• Adds comment explaining RHDH dynamic plugin configuration alternative

workspaces/x2a/packages/app/src/App.tsx


21. workspaces/x2a/plugins/x2a-common/package.json Dependencies +3/-1

CSV parsing library dependencies

• Adds papaparse dependency (^5.5.3) for CSV parsing functionality
• Adds @types/papaparse dev dependency (^5.5.2) for TypeScript support

workspaces/x2a/plugins/x2a-common/package.json


22. workspaces/x2a/plugins/x2a-common/report.api.md Miscellaneous +12/-0

Common package API exports for CSV functionality

• Exports new parseCsvContent function to parse base64-encoded CSV data URLs
• Exports new CsvProjectRow type alias for project creation request body
• Exports new allProviders constant array of available SCM providers
• Exports new SCAFFOLDER_SECRET_PREFIX constant for token storage naming

workspaces/x2a/plugins/x2a-common/report.api.md


23. workspaces/x2a/packages/app/package.json Dependencies +1/-0

Scaffolder React plugin dependency

• Adds @backstage/plugin-scaffolder-react dependency (^1.20.0) for scaffolder field extension
 support

workspaces/x2a/packages/app/package.json


24. workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx Formatting +3/-2

React import refactoring for createContext

• Changes React.createContext to createContext import from React
• Refactors context creation to use named import instead of namespace import

workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx


25. workspaces/x2a/plugins/x2a/package.json Dependencies +1/-0

Scaffolder React plugin dependency

• Adds @backstage/plugin-scaffolder-react dependency (^1.20.0) for scaffolder field extension
 components

workspaces/x2a/plugins/x2a/package.json


26. workspaces/x2a/README.md 📝 Documentation +4/-0

README documentation link for CSV import

• Adds new section referencing CSV Bulk Project Import documentation
• Links to detailed guide at ./docs/csv-bulk-import.md for file format and extension details

workspaces/x2a/README.md


27. workspaces/x2a/.changeset/wise-pianos-shine.md Miscellaneous +7/-0

Changeset for bulk CSV project creation

• Changeset entry documenting bulk CSV project creation feature
• Marks patches for scaffolder backend module, common package, and main x2a plugin

workspaces/x2a/.changeset/wise-pianos-shine.md


28. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts Additional files +0/-993

...

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts


29. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts Additional files +0/-229

...

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts


Grey Divider

Qodo Logo

@rhdh-qodo-merge
Copy link

rhdh-qodo-merge bot commented Mar 21, 2026

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (0) 📎 Requirement gaps (1) 📐 Spec deviations (0)

Grey Divider


Action required

1. Stale auth effect updates 🐞 Bug ✓ Correctness ⭐ New
Description
RepoAuthentication starts an async authentication flow in a useEffect without any
cancellation/guard, so if csvContent changes while authentication is in-flight, the earlier promise
can still set secrets and call onChange('authenticated'). This can unblock the wizard and persist
tokens that correspond to a previous CSV file.
Code

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[R75-147]

+  useEffect(() => {
+    if (!csvContent || suppressDialog || isDone) {
+      return;
+    }
+
+    setError(undefined);
+
+    let projectsToCreate;
+    try {
+      projectsToCreate = parseCsvContent(csvContent);
+    } catch (e) {
+      setError(e instanceof Error ? e.message : 'Unknown error');
+      return;
+    }
+
+    const allTargetProviders: ScmProvider[] = projectsToCreate.map(project =>
+      resolveScmProvider(project.targetRepoUrl, hostProviderMap),
+    );
+    const allSourceProviders: ScmProvider[] = projectsToCreate.map(project =>
+      resolveScmProvider(project.sourceRepoUrl, hostProviderMap),
+    );
+    const distinctTargetProviders = allTargetProviders.filter(
+      (p, i, arr) => arr.findIndex(q => q.name === p.name) === i,
+    );
+    const distinctSourceProviders = allSourceProviders.filter(
+      (p, i, arr) =>
+        arr.findIndex(q => q.name === p.name) === i &&
+        !distinctTargetProviders.some(t => t.name === p.name),
+    );
+    const allDistinctProviders = [
+      ...distinctTargetProviders,
+      ...distinctSourceProviders,
+    ];
+
+    const doAuthAsync = async () => {
+      const providerTokens = new Map<string, string>();
+
+      const authenticateProvider = async (
+        provider: ScmProvider,
+        readOnly: boolean,
+      ) => {
+        try {
+          const tokens = await repoAuthentication.authenticate([
+            provider.getAuthTokenDescriptor(readOnly),
+          ]);
+          providerTokens.set(
+            `${SCAFFOLDER_SECRET_PREFIX}${provider.name}`,
+            tokens[0].token,
+          );
+        } catch (e) {
+          setError(e instanceof Error ? e.message : 'Unknown error');
+          setSuppressDialog(true);
+        }
+      };
+
+      await Promise.all([
+        ...distinctTargetProviders.map(p => authenticateProvider(p, false)),
+        ...distinctSourceProviders.map(p => authenticateProvider(p, true)),
+      ]);
+
+      if (providerTokens.size === allDistinctProviders.length) {
+        onChange('authenticated');
+        setDone(true);
+        setSecrets({
+          ...secretsRef.current,
+          ...Object.fromEntries(providerTokens),
+        });
+      } else {
+        onChange(undefined);
+      }
+    };
+
+    doAuthAsync();
Evidence
One effect explicitly resets state when csvContent changes, but the authentication effect launches
doAuthAsync() and never cancels it. Since promises are not cancellable by default, a prior
in-flight doAuthAsync can resolve after the reset and still call onChange('authenticated') /
setSecrets.

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[65-72]
workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[75-156]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The `RepoAuthentication` field triggers async auth (`doAuthAsync`) inside a `useEffect` but does not guard against stale completion. If the user uploads a different CSV before auth finishes, the previous auth flow can still mark the field authenticated and write secrets.

### Issue Context
There is a separate effect that resets state on `csvContent` change, but it does not cancel in-flight auth.

### Fix Focus Areas
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[65-156]

### Suggested fix sketch
- Add a cancellation flag in the auth effect:
 - `let cancelled = false;`
 - In `doAuthAsync`, before calling `onChange/setDone/setSecrets`, check `if (cancelled) return;`
 - Return a cleanup function: `return () =&gt; { cancelled = true; }`
- Optionally also capture the current `csvContent` into a local variable and confirm it still matches `prevCsvRef.current` before committing updates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. No bulk API endpoint 📎 Requirement gap ✓ Correctness
Description
Bulk creation is implemented only as a Scaffolder action that loops over rows and calls existing
single-project APIs, so there is no standalone backend endpoint to bulk-create projects without the
UI. This prevents external clients/automation from invoking bulk creation directly as required.
Code

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[R269-366]

+        // CSV bulk import — tokens arrive pre-augmented from the RepoAuthentication
+        // frontend extension (which calls provider.augmentToken before storing them
+        // as scaffolder secrets), so no augmentToken call is needed here unlike the
+        // manual flow above.
+        const projectsToCreate = parseCsvContent(ctx.input.csvContent);
+
+        const providerTokens = new Map<ScmProviderName, string>();
+        Object.entries(ctx.secrets ?? {}).forEach(([key, value]) => {
+          if (key.startsWith(SCAFFOLDER_SECRET_PREFIX)) {
+            const providerName = key.replace(SCAFFOLDER_SECRET_PREFIX, '');
+            if (
+              allProviders.some(
+                (provider: ScmProvider) => provider.name === providerName,
+              )
+            ) {
+              providerTokens.set(providerName as ScmProviderName, value);
+            }
+          }
+        });
+
+        if (providerTokens.size === 0) {
+          throw new Error(
+            'At least one SCM provider authentication token is required for CSV import',
+          );
+        }
+
+        let successCount = 0;
+        let errorCount = 0;
+        let skippedCount = 0;
+
+        // Create projects sequentially to avoid overwhelming the API
+        for (const row of projectsToCreate) {
+          if (existingProjectNames.has(row.name)) {
+            ctx.logger.warn(
+              `Skipping project "${row.name}": a project with this name already exists. ` +
+                `To import it anyway, either change the project name or delete the existing project.`,
+            );
+            skippedCount++;
+            continue;
+          }
+
+          const sourceProvider = resolveScmProvider(
+            row.sourceRepoUrl,
+            hostProviderMap,
+          );
+          const sourceRepoToken = providerTokens.get(sourceProvider.name);
+          if (!sourceRepoToken) {
+            ctx.logger.error(
+              `Skipping project "${row.name}": no ${sourceProvider.name} authentication token provided (source: ${row.sourceRepoUrl})`,
+            );
+            errorCount++;
+            continue;
+          }
+
+          const targetUrl = row.targetRepoUrl ?? row.sourceRepoUrl;
+          const targetProvider = resolveScmProvider(targetUrl, hostProviderMap);
+          const targetRepoToken = providerTokens.get(targetProvider.name);
+          if (!targetRepoToken) {
+            ctx.logger.error(
+              `Skipping project "${row.name}": no ${targetProvider.name} authentication token provided (target: ${targetUrl})`,
+            );
+            errorCount++;
+            continue;
+          }
+
+          try {
+            await createAndInitProject({
+              api,
+              row,
+              sourceRepoToken,
+              targetRepoToken,
+              userPrompt: ctx.input.userPrompt,
+              backstageToken: token,
+              hostProviderMap,
+              logger: ctx.logger,
+            });
+            existingProjectNames.add(row.name);
+            successCount++;
+          } catch {
+            errorCount++;
+          }
+        }
+
+        ctx.logger.info(
+          `Bulk CSV import complete: ${successCount} succeeded, ${errorCount} failed, ${skippedCount} skipped out of ${projectsToCreate.length} project(s)`,
+        );
+
+        ctx.output('successCount', successCount);
+        ctx.output('errorCount', errorCount);
+        ctx.output('skippedCount', skippedCount);
+        ctx.output('nextUrl', '/x2a/projects');
+
+        if (errorCount > 0) {
+          throw new Error(
+            `CSV import completed with errors: ${successCount} succeeded, ${errorCount} failed, ${skippedCount} skipped out of ${projectsToCreate.length} project(s)`,
+          );
+        }
+      }
Evidence
PR Compliance ID 2 requires a new bulk project creation API endpoint callable independently of the
UI. The added x2a:project:create Scaffolder action performs CSV bulk creation by iterating rows
and calling createAndInitProject for each, which in turn calls the existing per-project endpoints
projectsPost and projectsProjectIdRunPost, rather than exposing a dedicated bulk-create API
endpoint.

Provide an API endpoint for bulk project creation callable without the UI
workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[269-366]
workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[45-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR adds CSV bulk creation via a Scaffolder action, but does not add a standalone backend API endpoint for bulk project creation callable without the UI.

## Issue Context
The current implementation loops over CSV rows and calls existing single-project endpoints (`projectsPost` and `projectsProjectIdRunPost`). Compliance requires a dedicated API endpoint that external clients can call directly to create multiple projects in one operation.

## Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[269-366]
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[45-92]
- workspaces/x2a/plugins/x2a-backend/src/router/projects.ts[1-400]
- workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/router.ts[36-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Node Buffer used in frontend 🐞 Bug ✓ Correctness
Description
parseCsvContent decodes the base64 payload using global Buffer, but RepoAuthentication calls
parseCsvContent in the browser; if Buffer isn’t provided by the frontend bundle, CSV parsing will
crash and the CSV import wizard can’t proceed.
Code

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[R57-65]

+export function parseCsvContent(dataUrl: string): CsvProjectRow[] {
+  const base64Match = dataUrl.match(/^data:(?:[^;]*;)*base64,(.*)$/);
+  if (!base64Match) {
+    throw new Error('Invalid CSV content: expected a base64-encoded data-URL');
+  }
+
+  const csvText = Buffer.from(base64Match[1], 'base64').toString('utf-8');
+
+  const result = Papa.parse<Record<string, string>>(csvText, {
Evidence
The shared CSV parser decodes via Buffer (Node API) and is directly invoked from the frontend field
extension during CSV import, but the shared package does not declare any browser Buffer/polyfill
dependency.

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[57-66]
workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[70-83]
workspaces/x2a/plugins/x2a-common/package.json[51-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`parseCsvContent` uses `Buffer.from(..., &#x27;base64&#x27;)` to decode the data-URL payload. This assumes a Node/global `Buffer`, but `parseCsvContent` is also called from the frontend (`RepoAuthentication`), where `Buffer` is not a standard browser global.

### Issue Context
`parseCsvContent` is in `x2a-common` (shared) and is used both server-side and client-side. It should use a decoding approach that works in both environments, or explicitly import a polyfill.

### Fix Focus Areas
- workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[57-66]
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[70-83]

### Suggested implementation direction
- Replace `Buffer.from(...).toString(&#x27;utf-8&#x27;)` with a small helper:
 - If `globalThis.Buffer` exists, use it.
 - Else use `atob` + `TextDecoder(&#x27;utf-8&#x27;)` to decode base64 into UTF-8.
- Keep the rest of the parsing logic unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
4. Auth not rerun on CSV change 🐞 Bug ✓ Correctness
Description
RepoAuthentication stops re-authentication permanently after the first success (isDone), so
uploading a different CSV later in the wizard won’t trigger authentication for newly introduced SCM
providers and the backend CSV import will fail due to missing provider tokens.
Code

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[R63-129]

+  useEffect(() => {
+    if (!csvContent || suppressDialog || isDone) {
+      return;
+    }
+
+    setError(undefined);
+
+    let projectsToCreate;
+    try {
+      projectsToCreate = parseCsvContent(csvContent);
+    } catch (e) {
+      setError(e instanceof Error ? e.message : 'Unknown error');
+      return;
+    }
+
+    const allTargetProviders: ScmProvider[] = projectsToCreate.map(project =>
+      resolveScmProvider(project.targetRepoUrl, hostProviderMap),
+    );
+    const allSourceProviders: ScmProvider[] = projectsToCreate.map(project =>
+      resolveScmProvider(project.sourceRepoUrl, hostProviderMap),
+    );
+    const distinctTargetProviders = allTargetProviders.filter(
+      (p, i, arr) => arr.findIndex(q => q.name === p.name) === i,
+    );
+    const distinctSourceProviders = allSourceProviders.filter(
+      (p, i, arr) =>
+        arr.findIndex(q => q.name === p.name) === i &&
+        !distinctTargetProviders.some(t => t.name === p.name),
+    );
+    const allDistinctProviders = [
+      ...distinctTargetProviders,
+      ...distinctSourceProviders,
+    ];
+
+    const doAuthAsync = async () => {
+      const providerTokens = new Map<string, string>();
+
+      const authenticateProviders = (
+        providers: ScmProvider[],
+        readOnly: boolean,
+      ) =>
+        providers.map(provider =>
+          repoAuthentication
+            .authenticate([provider.getAuthTokenDescriptor(readOnly)])
+            .then(tokens => {
+              providerTokens.set(
+                `${SCAFFOLDER_SECRET_PREFIX}${provider.name}`,
+                tokens[0].token,
+              );
+            })
+            .catch(e => {
+              setError(e instanceof Error ? e.message : 'Unknown error');
+              setSuppressDialog(true);
+            }),
+        );
+
+      await Promise.all([
+        ...authenticateProviders(distinctTargetProviders, false),
+        ...authenticateProviders(distinctSourceProviders, true),
+      ]);
+
+      if (providerTokens.size === allDistinctProviders.length) {
+        onChange('authenticated');
+        setDone(true);
+      } else {
+        onChange(undefined);
+      }
Evidence
The effect exits early when isDone is true, and isDone is set to true after the first successful
authentication. There is no reset path when csvContent changes, so subsequent CSV changes won’t
re-run auth/provider detection.

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[63-66]
workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[124-129]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`RepoAuthentication` sets `isDone=true` after a successful auth, and the main `useEffect` returns early if `isDone` is true. This prevents re-auth when the user replaces/edits the uploaded CSV (which can change the set of required SCM providers).

### Issue Context
The feature is explicitly designed to support re-running imports and correcting inputs; the UI must also handle users changing the CSV in the wizard.

### Fix Focus Areas
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[63-66]
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[124-129]

### Suggested implementation direction
- Track the last processed `csvContent` (or a derived provider-set key) in a ref/state.
- When `csvContent` changes, reset `isDone` (and optionally `suppressDialog`/`error`) so authentication runs again for the new provider set.
- Ensure `onChange(undefined)` is emitted when switching CSVs until auth succeeds for the new CSV.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Manual output counts undefined 🐞 Bug ✓ Correctness
Description
In manual mode the action only outputs successCount, but the template always renders skippedCount
and errorCount, resulting in user-visible undefined values (and a schema/output contract mismatch).
Code

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[R263-268]

+        ctx.output('projectId', result.projectId);
+        ctx.output('initJobId', result.initJobId);
+        ctx.output('successCount', 1);
+        // no need for skippedCount and errorCount
+        ctx.output('nextUrl', `/x2a/projects/${result.projectId}`);
+      } else {
Evidence
The manual branch intentionally omits skippedCount and errorCount, while the template output
block always prints both values. This will display as empty/undefined for manual runs.

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[263-268]
workspaces/x2a/templates/conversion-project-template.yaml[228-233]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Manual project creation outputs `successCount` but not `skippedCount`/`errorCount`, while the template output always references `skippedCount` and `errorCount`. This leads to confusing output for users (e.g., “Skipped: undefined”).

### Issue Context
The template’s `output.text` block is shared for both manual and CSV modes.

### Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[263-268]
- workspaces/x2a/templates/conversion-project-template.yaml[228-233]

### Suggested implementation direction
- In the manual branch, add:
 - `ctx.output(&#x27;skippedCount&#x27;, 0)`
 - `ctx.output(&#x27;errorCount&#x27;, 0)`
- (Optional) Consider making `projectId`/`initJobId` outputs optional in the action schema if they are not emitted in CSV mode, but the immediate user-facing issue is the missing counts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Duplicate-check ignores non-OK 🐞 Bug ⛯ Reliability
Description
fetchExistingProjectNames doesn’t check response.ok, so if /projects returns a non-2xx response the
code will treat the error JSON as data and likely return an empty set, silently disabling duplicate
detection and “skip existing” CSV reruns.
Code

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[R43-52]

+  for (;;) {
+    const response = await api.projectsGet(
+      { query: { page, pageSize } },
+      { token },
+    );
+    const data = await response.json();
+
+    if (!data.items || data.items.length === 0) {
+      break;
+    }
Evidence
DefaultApiClient.projectsGet returns the raw fetch Response regardless of status;
fetchExistingProjectNames unconditionally calls response.json() and then stops when data.items is
missing, producing an incomplete/empty name set without surfacing the failure.

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[43-52]
workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/apis/Api.client.ts[214-234]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`fetchExistingProjectNames` assumes `/projects` always returns a valid success payload. On non-2xx responses, it still calls `response.json()` and then exits when `data.items` is missing, silently returning an empty set. This breaks:
- manual duplicate-name pre-check
- CSV ‘skip existing’ behavior for re-runs

### Issue Context
`DefaultApiClient.projectsGet` returns the raw `fetch` response and does not throw on non-OK.

### Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts[35-64]

### Suggested implementation direction
- Add `if (!response.ok)` handling:
 - Parse the error body (best-effort) and throw a clear error like: “Unable to list existing projects (status X): ...”.
 - Alternatively, log a warning and proceed, but do so explicitly so operators/users understand that duplicate-skipping is disabled.
- Consider adding tests for non-OK responses to ensure behavior is intentional and visible.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

7. Empty ownedByGroup sent 🐞 Bug ✓ Correctness
Description
createAndInitProject uses row.ownedByGroup?.trim() ?? undefined, which turns whitespace-only
values into an empty string and sends that to the API instead of omitting ownedByGroup.
Code

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[R45-52]

+  const body: ProjectsPost['body'] = {
+    name: row.name,
+    description: row.description ?? '',
+    abbreviation: row.abbreviation,
+    ownedByGroup: row.ownedByGroup?.trim() ?? undefined,
+    sourceRepoUrl: normalizeRepoUrl(row.sourceRepoUrl),
+    targetRepoUrl: normalizeRepoUrl(row.targetRepoUrl ?? row.sourceRepoUrl),
+    sourceRepoBranch: row.sourceRepoBranch,
Evidence
?? undefined does not convert empty strings to undefined. The CSV parser already normalizes blank
ownedByGroup to undefined via || undefined, indicating the intended semantics are to omit blanks;
createAndInitProject should match that behavior.

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[45-52]
workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[110-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ownedByGroup` is currently computed with `row.ownedByGroup?.trim() ?? undefined`, which will keep `&#x27;&#x27;` (empty string) instead of omitting it. This can result in sending an empty `ownedByGroup` to the backend.

### Issue Context
The CSV parsing path already uses `|| undefined` for `ownedByGroup`, so the create call should align.

### Fix Focus Areas
- workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts[45-52]

### Suggested implementation direction
- Change to `ownedByGroup: row.ownedByGroup?.trim() || undefined`.
- (Optional) Add/extend a unit test to cover whitespace-only `ownedByGroup` and assert it is omitted from the request body.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

8. CSV header help text wrong 🐞 Bug ⚙ Maintainability
Description
The template’s CSV upload description lists targetRepoBranch as both required and optional,
contradicting the actual parser requirements and confusing users about valid CSV format.
Code

workspaces/x2a/templates/conversion-project-template.yaml[R92-95]

+                      The CSV file must contain the following headers: name, abbreviation, sourceRepoUrl, sourceRepoBranch, targetRepoBranch.
+
+                      Optional headers: description, ownedByGroup, targetRepoUrl, targetRepoBranch.
+
Evidence
The template help text contradicts itself, while the parser clearly defines targetRepoBranch as
required and does not include it in optional headers.

workspaces/x2a/templates/conversion-project-template.yaml[92-95]
workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts[27-39]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The CSV upload help text states `targetRepoBranch` is required and also lists it under optional headers. This is inconsistent with the parser and can cause users to build invalid CSVs.

### Issue Context
`parseCsvContent` treats `targetRepoBranch` as required.

### Fix Focus Areas
- workspaces/x2a/templates/conversion-project-template.yaml[92-95]

### Suggested implementation direction
- Remove `targetRepoBranch` from the optional headers list in the description so it matches the parser’s required/optional header sets.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +75 to +147
useEffect(() => {
if (!csvContent || suppressDialog || isDone) {
return;
}

setError(undefined);

let projectsToCreate;
try {
projectsToCreate = parseCsvContent(csvContent);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
return;
}

const allTargetProviders: ScmProvider[] = projectsToCreate.map(project =>
resolveScmProvider(project.targetRepoUrl, hostProviderMap),
);
const allSourceProviders: ScmProvider[] = projectsToCreate.map(project =>
resolveScmProvider(project.sourceRepoUrl, hostProviderMap),
);
const distinctTargetProviders = allTargetProviders.filter(
(p, i, arr) => arr.findIndex(q => q.name === p.name) === i,
);
const distinctSourceProviders = allSourceProviders.filter(
(p, i, arr) =>
arr.findIndex(q => q.name === p.name) === i &&
!distinctTargetProviders.some(t => t.name === p.name),
);
const allDistinctProviders = [
...distinctTargetProviders,
...distinctSourceProviders,
];

const doAuthAsync = async () => {
const providerTokens = new Map<string, string>();

const authenticateProvider = async (
provider: ScmProvider,
readOnly: boolean,
) => {
try {
const tokens = await repoAuthentication.authenticate([
provider.getAuthTokenDescriptor(readOnly),
]);
providerTokens.set(
`${SCAFFOLDER_SECRET_PREFIX}${provider.name}`,
tokens[0].token,
);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
setSuppressDialog(true);
}
};

await Promise.all([
...distinctTargetProviders.map(p => authenticateProvider(p, false)),
...distinctSourceProviders.map(p => authenticateProvider(p, true)),
]);

if (providerTokens.size === allDistinctProviders.length) {
onChange('authenticated');
setDone(true);
setSecrets({
...secretsRef.current,
...Object.fromEntries(providerTokens),
});
} else {
onChange(undefined);
}
};

doAuthAsync();

Choose a reason for hiding this comment

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

Action required

1. Stale auth effect updates 🐞 Bug ✓ Correctness

RepoAuthentication starts an async authentication flow in a useEffect without any
cancellation/guard, so if csvContent changes while authentication is in-flight, the earlier promise
can still set secrets and call onChange('authenticated'). This can unblock the wizard and persist
tokens that correspond to a previous CSV file.
Agent Prompt
### Issue description
The `RepoAuthentication` field triggers async auth (`doAuthAsync`) inside a `useEffect` but does not guard against stale completion. If the user uploads a different CSV before auth finishes, the previous auth flow can still mark the field authenticated and write secrets.

### Issue Context
There is a separate effect that resets state on `csvContent` change, but it does not cancel in-flight auth.

### Fix Focus Areas
- workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx[65-156]

### Suggested fix sketch
- Add a cancellation flag in the auth effect:
  - `let cancelled = false;`
  - In `doAuthAsync`, before calling `onChange/setDone/setSecrets`, check `if (cancelled) return;`
  - Return a cleanup function: `return () => { cancelled = true; }`
- Optionally also capture the current `csvContent` into a local variable and confirm it still matches `prevCsvRef.current` before committing updates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@mareklibra mareklibra force-pushed the FLPATH-3408.bulkCSVProjectCreation branch from b5ebcf6 to 92bc819 Compare March 21, 2026 08:47
Signed-off-by: Marek Libra <marek.libra@gmail.com>
@mareklibra mareklibra force-pushed the FLPATH-3408.bulkCSVProjectCreation branch from 92bc819 to d77d33f Compare March 21, 2026 08:59
Copy link
Contributor

@elai-shalev elai-shalev left a comment

Choose a reason for hiding this comment

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

Functionality wise - all great.
Suggestions:
(1) Adding "Last used" CSVs in the UI for accessing, reviewing, an reusing. We should keep the CSV file in the Project card for later acess
(2) Cosmetics and UX - the flow for the CSV upload is very efficient - but has overhead with too many clicks and too wordy and unstructured message.

Overall great feature

Comment on lines +83 to +97
Upload a CSV file containing project definitions including source and target repositories.

Projects are created sequentially, with the same permissions and other checks applied to
each one - just as if the signed-in user had created them individually.

Projects whose name already exists are skipped. This means the import can be run
repeatedly: fix any failing rows in the CSV, delete the broken project if needed,
and re-upload — already-created projects will not be duplicated.

The CSV file must contain the following headers: name, abbreviation, sourceRepoUrl, sourceRepoBranch, targetRepoBranch.

Optional headers: description, ownedByGroup, targetRepoUrl.

The headers can be in any order, but the values must be in the same order as the headers.
The file must be encoded in UTF-8.
Copy link
Contributor

Choose a reason for hiding this comment

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

On the UI, this is a large chunk of text tat is hard to read. I think we need to (1) add a samle / skeleton CSV to ease the user into understanding the format and (2) reorganize the instructions a bit to make it more readable

Copy link
Member Author

Choose a reason for hiding this comment

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

Rephrasing and adding the example to download

repoAuthentication:
type: string
description: |
Provide login to all the SCMs relevant for the source CSV.
Copy link
Contributor

Choose a reason for hiding this comment

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

This step is a bit "annoying" - could we remove it to minimize the amount of clicks?
we can add this as a banner / warning message on the previous / last panel

@mareklibra
Copy link
Member Author

Hiding a step conditionally isn't supported by the scaffolder, so this is tricky.

As a workaround we can:

  • either split the template by use case (manual vs. CSV) which is not ideal,
  • or use a custom layout - requires writing a lot of code from our side.

Either way, shifting the new RepoAuthentication widget back to the "Job name and description" page isn't straightforward due to scaffolder data flow.

Moving it forward to the "prompt step" would be easier technically, but it doesn't fit logically.

If users get used to the manual flow, they should be ok with this step.
We could enhance the RepoAuthentication widget to list required SCM providers and read/write modes, so adding additional value and reasoning for this wizard step.

@mareklibra
Copy link
Member Author

mareklibra commented Mar 23, 2026

Updated screenshot:

image

@mareklibra mareklibra marked this pull request as draft March 23, 2026 09:26
Signed-off-by: Marek Libra <marek.libra@gmail.com>
@mareklibra mareklibra force-pushed the FLPATH-3408.bulkCSVProjectCreation branch from d298bb9 to 017fe25 Compare March 23, 2026 09:32
@mareklibra mareklibra marked this pull request as ready for review March 23, 2026 09:39
@rhdh-qodo-merge
Copy link

Review Summary by Qodo

Bulk CSV project creation with RepoAuthentication Scaffolder extension

✨ Enhancement 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• Implements bulk CSV project creation feature allowing users to upload a CSV file to create
  multiple projects in batch
• Introduces RepoAuthentication custom Scaffolder field extension that seamlessly requests tokens
  from relevant SCM providers (GitHub, GitLab, Bitbucket) with improved UX over standard RepoUrlPicker
• Adds comprehensive test coverage for project creation action (2179 lines) and CSV parsing
  functionality (410 lines)
• Implements parseCsvContent function for robust CSV parsing with validation of required columns
  (name, abbreviation, sourceRepoUrl, sourceRepoBranch, targetRepoBranch) and optional
  fields with defaults
• Supports automatic skipping of duplicate projects on re-runs, enabling safe retry of failed
  batches
• Includes per-provider token management and validation for multiple SCM providers
• Adds static file serving infrastructure for downloading sample CSV templates
• Provides comprehensive documentation for CSV bulk import workflow and RepoAuthentication
  extension usage
• Refactors project creation logic into createAndInitProject helper function with enhanced logging
• Updates conversion project template with conditional UI for manual vs CSV upload modes
Diagram
flowchart LR
  CSV["CSV File Upload"]
  Parser["parseCsvContent"]
  RepoAuth["RepoAuthentication Extension"]
  TokenMgmt["Token Management<br/>GitHub/GitLab/Bitbucket"]
  CreateAction["createProjectAction"]
  DupCheck["Duplicate Detection<br/>with Pagination"]
  CreateInit["createAndInitProject"]
  Result["Project Creation<br/>Results Summary"]
  
  CSV --> Parser
  Parser --> RepoAuth
  RepoAuth --> TokenMgmt
  TokenMgmt --> CreateAction
  CreateAction --> DupCheck
  DupCheck --> CreateInit
  CreateInit --> Result
Loading

Grey Divider

File Changes

Grey Divider

Qodo Logo

@rhdh-qodo-merge
Copy link

rhdh-qodo-merge bot commented Mar 23, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@elai-shalev
Copy link
Contributor

elai-shalev commented Mar 23, 2026

@mareklibra new text and example look good.
2 more points:
(1) Adding "Last used" CSVs in the UI for accessing, reviewing, an reusing. We should keep the CSV file in the Project card for later acess
(2) Could we add a cancel button / remove the CSV next to it?
image

@elai-shalev elai-shalev reopened this Mar 23, 2026
@rhdh-qodo-merge
Copy link

Review Summary by Qodo

Bulk CSV project creation with RepoAuthentication Scaffolder extension

✨ Enhancement 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• Implement bulk CSV project creation feature allowing users to upload a CSV file to create multiple
  projects at once
• Add new x2a:project:create Scaffolder action supporting both manual and CSV bulk import modes
  with duplicate detection and per-provider token handling
• Implement RepoAuthentication custom Scaffolder field extension for seamless multi-provider SCM
  authentication without requiring standard RepoUrlPicker
• Add CSV parser (parseCsvContent) with base64 data-URL decoding, header validation, and support
  for required/optional fields with sensible defaults
• Implement createAndInitProject helper function to create projects and trigger init-phase in a
  single operation
• Add static file serving middleware to backend for distributing sample CSV files
• Restructure conversion project template with radio button for input method selection and
  conditional form fields based on chosen method
• Add comprehensive test coverage for project creation action (2179 lines), CSV parsing (410 lines),
  and RepoAuthentication component (497 lines)
• Enable automatic retry capability for bulk imports - same batch can be re-run, automatically
  skipping projects that already exist
• Add detailed documentation for CSV bulk import feature including format specifications, column
  definitions, and usage examples
Diagram
flowchart LR
  CSV["CSV File Upload"]
  Parser["parseCsvContent<br/>Parser"]
  RepoAuth["RepoAuthentication<br/>Extension"]
  Action["x2a:project:create<br/>Action"]
  Helper["createAndInitProject<br/>Helper"]
  Backend["Backend Static<br/>File Serving"]
  
  CSV --> Parser
  Parser --> RepoAuth
  RepoAuth --> Action
  Action --> Helper
  Backend -.->|Sample CSV| CSV
Loading

Grey Divider

File Changes

1. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.test.ts 🧪 Tests +2179/-0

Complete test coverage for project creation action

• Comprehensive test suite (2179 lines) for the x2a:project:create Scaffolder action covering
 manual and CSV bulk import modes
• Tests for manual project creation with various input combinations, token augmentation for
 different SCM providers (GitHub, GitLab, Bitbucket)
• CSV bulk import tests including per-provider token handling, duplicate detection, pagination, and
 error scenarios
• Tests for project name collision detection and skipping of existing projects in bulk imports

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.test.ts


2. workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.test.ts 🧪 Tests +410/-0

CSV content parsing and validation test suite

• Test suite (410 lines) for CSV parsing functionality with base64 data-URL decoding
• Tests for header validation, required/optional field handling, and edge cases (quoted fields,
 UTF-8, CRLF line endings)
• Tests for column ordering flexibility and default value handling (e.g., targetRepoUrl defaults
 to sourceRepoUrl)
• Tests for browser environment fallback when Buffer is unavailable

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.test.ts


3. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts ✨ Enhancement +462/-0

Scaffolder action for project creation with bulk CSV support

• New Scaffolder action x2a:project:create supporting both manual and CSV bulk import modes
• Implements project creation with duplicate name detection via paginated API calls
• Handles per-provider token extraction and augmentation for GitHub, GitLab, and Bitbucket
• Supports optional ownedByGroup field and user prompts for the init phase

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.ts


View more (33)
4. workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts ✨ Enhancement +141/-0

CSV bulk import file parser with data-URL decoding

• CSV parser using PapaParse to decode base64 data-URL encoded CSV content
• Validates required columns (name, abbreviation, sourceRepoUrl, sourceRepoBranch,
 targetRepoBranch)
• Handles optional columns (description, ownedByGroup, targetRepoUrl) with sensible defaults
• Supports browser and Node.js environments with fallback decoding via atob when Buffer is
 unavailable

workspaces/x2a/plugins/x2a-common/src/csv/parseCsvContent.ts


5. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts ✨ Enhancement +120/-0

Project creation and initialization helper function

• Helper function to create a project and trigger its init-phase in a single operation
• Normalizes repository URLs and trims optional fields before API submission
• Handles API responses and error logging for both project creation and init-phase steps
• Returns project ID and init job ID for downstream processing

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createAndInitProject.ts


6. workspaces/x2a/plugins/x2a-backend/src/router/index.ts ✨ Enhancement +8/-0

Static file serving for backend plugin

• Adds static file serving middleware for the public directory
• Uses resolvePackagePath to locate the public directory relative to the backend plugin package
• Mounts static files at /static route prefix

workspaces/x2a/plugins/x2a-backend/src/router/index.ts


7. workspaces/x2a/plugins/x2a/src/index.ts ✨ Enhancement +5/-4

Export RepoAuthentication Scaffolder field extension

• Exports new RepoAuthenticationExtension for Scaffolder field extension
• Reorganizes exports to consolidate useTranslation hook and its type in a single statement

workspaces/x2a/plugins/x2a/src/index.ts


8. workspaces/x2a/plugins/x2a/src/plugin.ts ✨ Enhancement +11/-0

RepoAuthentication Scaffolder field extension registration

• Registers RepoAuthenticationExtension as a Scaffolder field extension with custom validation
• Imports createScaffolderFieldExtension from Scaffolder React plugin
• Provides seamless token collection UI for multiple SCM providers in Scaffolder templates

workspaces/x2a/plugins/x2a/src/plugin.ts


9. workspaces/x2a/plugins/x2a-common/src/csv/index.ts ✨ Enhancement +16/-0

CSV module public API exports

• New barrel export file for CSV parsing functionality
• Exports parseCsvContent function and CsvProjectRow type for public API

workspaces/x2a/plugins/x2a-common/src/csv/index.ts


10. workspaces/x2a/plugins/x2a-common/src/constants.ts ⚙️ Configuration changes +7/-0

SCM provider token secret key prefix constant

• Adds SCAFFOLDER_SECRET_PREFIX constant set to 'OAUTH_TOKEN_'
• Used as the prefix for SCM provider authentication token secret keys in Scaffolder templates

workspaces/x2a/plugins/x2a-common/src/constants.ts


11. workspaces/x2a/plugins/x2a/src/scaffolder/index.ts ✨ Enhancement +15/-0

Export RepoAuthentication scaffolder extension

• New file exporting the RepoAuthentication component
• Provides public API for the custom scaffolder field extension

workspaces/x2a/plugins/x2a/src/scaffolder/index.ts


12. workspaces/x2a/plugins/x2a-common/src/scm/providerRegistry.ts ✨ Enhancement +6/-1

Export allProviders SCM provider list

• Export allProviders constant with JSDoc documentation
• Makes SCM provider list publicly available for use in other modules

workspaces/x2a/plugins/x2a-common/src/scm/providerRegistry.ts


13. workspaces/x2a/plugins/x2a-backend/src/plugin.ts ⚙️ Configuration changes +5/-0

Allow unauthenticated access to static files

• Add unauthenticated HTTP policy for /static/:file endpoint
• Enables serving static files (like sample CSV) without authentication

workspaces/x2a/plugins/x2a-backend/src/plugin.ts


14. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/module.ts Miscellaneous +1/-1

Update createProject action import path

• Update import path from ./actions/createProject to ./actions/createProjectAction
• Aligns with renamed action file

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/module.ts


15. workspaces/x2a/plugins/x2a-common/src/scm/index.ts ✨ Enhancement +1/-0

Export allProviders from scm module

• Export allProviders from providerRegistry module
• Makes provider list available to consumers of x2a-common

workspaces/x2a/plugins/x2a-common/src/scm/index.ts


16. workspaces/x2a/plugins/x2a/src/routes.ts ✨ Enhancement +6/-0

Add download route reference

• Add new downloadRouteRef sub-route for /download path
• Enables file download functionality in the x2a plugin

workspaces/x2a/plugins/x2a/src/routes.ts


17. workspaces/x2a/plugins/x2a-common/src/index.ts ✨ Enhancement +1/-0

Export CSV utilities from common module

• Export all CSV-related utilities from the csv module
• Provides public API for CSV parsing and validation

workspaces/x2a/plugins/x2a-common/src/index.ts


18. workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.test.tsx 🧪 Tests +497/-0

Add RepoAuthentication component tests

• Comprehensive test suite for RepoAuthentication component (497 lines)
• Tests CSV parsing, authentication flow, error handling, and validation
• Covers single/multiple providers, cross-provider scenarios, and retry logic

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.test.tsx


19. workspaces/x2a/templates/conversion-project-template.yaml ✨ Enhancement +145/-58

Restructure template for CSV bulk import support

• Add inputMethod radio button to choose between manual entry or CSV upload
• Restructure form with conditional dependencies based on input method
• Add csvContent field for file upload with format validation
• Add repoAuthentication field for SCM provider authentication
• Update template action to pass inputMethod and csvContent parameters
• Update results display to show success/skipped/error counts

workspaces/x2a/templates/conversion-project-template.yaml


20. workspaces/x2a/docs/csv-bulk-import.md 📝 Documentation +132/-0

Add CSV bulk import documentation

• New comprehensive documentation for CSV bulk import feature (132 lines)
• Includes CSV file format, required/optional columns, and URL format conventions
• Provides example CSV with multiple providers and repeatable import workflow
• Documents RepoAuthentication scaffolder extension and registration

workspaces/x2a/docs/csv-bulk-import.md


21. workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx ✨ Enhancement +234/-0

Implement RepoAuthentication scaffolder extension

• Implement RepoAuthentication custom scaffolder field component (234 lines)
• Parses CSV content, identifies distinct SCM providers, and orchestrates authentication
• Stores tokens as scaffolder secrets with OAUTH_TOKEN_ prefix
• Handles errors and provides retry mechanism
• Export repoAuthenticationValidation function to block progression until authenticated

workspaces/x2a/plugins/x2a/src/scaffolder/RepoAuthentication.tsx


22. workspaces/x2a/plugins/x2a/report.api.md Miscellaneous +11/-7

Update API report with RepoAuthentication export

• Add RepoAuthenticationExtension to public API exports
• Update import paths for translation references from frontend-plugin-api to
 core-plugin-api/alpha
• Reorder translation keys in report

workspaces/x2a/plugins/x2a/report.api.md


23. workspaces/x2a/plugins/x2a/src/components/DownloadStaticPublicFile.test.tsx 🧪 Tests +85/-0

Add DownloadStaticPublicFile component tests

• Test suite for DownloadStaticPublicFile component (85 lines)
• Tests redirect to backend static URL, nested file paths, and edge cases
• Verifies component renders nothing and properly constructs download URLs

workspaces/x2a/plugins/x2a/src/components/DownloadStaticPublicFile.test.tsx


24. workspaces/x2a/packages/app/src/App.tsx ✨ Enhancement +11/-1

Register RepoAuthentication extension in app

• Import ScaffolderFieldExtensions and RepoAuthenticationExtension
• Wrap /create route with ScaffolderFieldExtensions containing RepoAuthenticationExtension
• Add comment explaining RHDH dynamic plugin configuration override

workspaces/x2a/packages/app/src/App.tsx


25. workspaces/x2a/plugins/x2a/src/components/DownloadStaticPublicFile.tsx ✨ Enhancement +35/-0

Add DownloadStaticPublicFile redirect component

• New component that redirects /x2a/download/* requests to /api/x2a/static/*
• Uses discovery API to construct backend URL and redirects via location.href
• Enables serving static files like sample CSV from backend

workspaces/x2a/plugins/x2a/src/components/DownloadStaticPublicFile.tsx


26. workspaces/x2a/plugins/x2a/src/components/Router.tsx ✨ Enhancement +6/-1

Add download route to plugin router

• Import DownloadStaticPublicFile component and downloadRouteRef
• Add route for /download/* path using DownloadStaticPublicFile component

workspaces/x2a/plugins/x2a/src/components/Router.tsx


27. workspaces/x2a/plugins/x2a-common/package.json Dependencies +3/-1

Add papaparse CSV parsing dependency

• Add papaparse dependency for CSV parsing
• Add @types/papaparse dev dependency for TypeScript support

workspaces/x2a/plugins/x2a-common/package.json


28. workspaces/x2a/plugins/x2a-common/report.api.md Miscellaneous +12/-0

Update API report with CSV utilities

• Export allProviders SCM provider array
• Export CsvProjectRow type alias for project data
• Export parseCsvContent function for CSV parsing
• Export SCAFFOLDER_SECRET_PREFIX constant for token storage

workspaces/x2a/plugins/x2a-common/report.api.md


29. workspaces/x2a/packages/app/package.json Dependencies +1/-0

Add scaffolder-react dependency

• Add @backstage/plugin-scaffolder-react dependency for scaffolder field extensions

workspaces/x2a/packages/app/package.json


30. workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx Formatting +3/-2

Refactor React.createContext to named import

• Change React.createContext to createContext import from React
• Improves code consistency by using named import instead of namespace

workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx


31. workspaces/x2a/plugins/x2a/package.json Dependencies +1/-0

Add scaffolder-react dependency

• Add @backstage/plugin-scaffolder-react dependency for scaffolder field extensions

workspaces/x2a/plugins/x2a/package.json


32. workspaces/x2a/plugins/x2a-backend/package.json ⚙️ Configuration changes +1/-0

Include public directory in backend distribution

• Add public directory to files array for distribution
• Enables inclusion of static files (sample CSV) in published package

workspaces/x2a/plugins/x2a-backend/package.json


33. workspaces/x2a/.changeset/wise-pianos-shine.md Miscellaneous +8/-0

Add changeset for bulk CSV feature

• New changeset documenting bulk CSV project creation feature
• Marks patch version bump for all x2a packages

workspaces/x2a/.changeset/wise-pianos-shine.md


34. workspaces/x2a/README.md 📝 Documentation +4/-0

Add CSV bulk import documentation reference

• Add section linking to CSV bulk import documentation
• References new csv-bulk-import.md guide with format and extension details

workspaces/x2a/README.md


35. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts Additional files +0/-993

...

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts


36. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts Additional files +0/-229

...

workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts


Grey Divider

Qodo Logo

@rhdh-qodo-merge
Copy link

Code Review by Qodo

Grey Divider

Looking for bugs?

Check back in a few minutes. An AI review agent is analyzing this pull request.

Grey Divider

Qodo Logo

@sonarqubecloud
Copy link

@mareklibra mareklibra merged commit e9f35e2 into redhat-developer:main Mar 23, 2026
22 of 24 checks passed
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