feat(x2a): bulk CSV project creation#2579
Conversation
Changed Packages
|
Review Summary by QodoAdd bulk CSV project creation with RepoAuthentication scaffolder extension
WalkthroughsDescription• 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 Diagramflowchart 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"]
File Changes1. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.test.ts
|
Code Review by Qodo
1. No bulk API endpoint
|
33a2ee5 to
7d98a81
Compare
| // 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)`, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- https://docs.qodo.ai/qodo-documentation/code-review/qodo-merge/tools/improve#extra-instructions-and-best-practices
- https://docs.qodo.ai/qodo-documentation/code-review/qodo-merge/tools/improve#extra-instructions
- https://docs.qodo.ai/qodo-documentation/code-review/qodo-merge/usage-guide/additional_configurations#extra-instructions
| 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, { |
There was a problem hiding this comment.
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
| 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); | ||
| } |
There was a problem hiding this comment.
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
| 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 { |
There was a problem hiding this comment.
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
| for (;;) { | ||
| const response = await api.projectsGet( | ||
| { query: { page, pageSize } }, | ||
| { token }, | ||
| ); | ||
| const data = await response.json(); | ||
|
|
||
| if (!data.items || data.items.length === 0) { | ||
| break; | ||
| } |
There was a problem hiding this comment.
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
7d98a81 to
b5ebcf6
Compare
Review Summary by QodoBulk CSV project creation with RepoAuthentication Scaffolder extension
WalkthroughsDescription• 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 Diagramflowchart 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
File Changes1. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.test.ts
|
Code Review by Qodo
1. Stale auth effect updates
|
| 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(); |
There was a problem hiding this comment.
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
b5ebcf6 to
92bc819
Compare
Signed-off-by: Marek Libra <marek.libra@gmail.com>
92bc819 to
d77d33f
Compare
elai-shalev
left a comment
There was a problem hiding this comment.
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
| 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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Rephrasing and adding the example to download
| repoAuthentication: | ||
| type: string | ||
| description: | | ||
| Provide login to all the SCMs relevant for the source CSV. |
There was a problem hiding this comment.
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
|
Hiding a step conditionally isn't supported by the scaffolder, so this is tricky. As a workaround we can:
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. |
Signed-off-by: Marek Libra <marek.libra@gmail.com>
d298bb9 to
017fe25
Compare
Review Summary by QodoBulk CSV project creation with RepoAuthentication Scaffolder extension
WalkthroughsDescription• 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 Diagramflowchart 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
File Changes |
Code Review by QodoNew Review StartedThis review has been superseded by a new analysisⓘ The new review experience is currently in Beta. Learn more |
|
@mareklibra new text and example look good. |
Review Summary by QodoBulk CSV project creation with RepoAuthentication Scaffolder extension
WalkthroughsDescription• 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 Diagramflowchart 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
File Changes1. workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProjectAction.test.ts
|
|






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
RepoAuthenticationcustom 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