diff --git a/ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/create-run/-components/default-values.ts b/ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/create-run/-components/default-values.ts index 5417ff6a90..d9319809bd 100644 --- a/ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/create-run/-components/default-values.ts +++ b/ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/create-run/-components/default-values.ts @@ -239,7 +239,8 @@ export function defaultValues( baseDefaults.jobConfigs.advisor.advisors, config: mergePluginConfigs( ortRun?.jobConfigs?.advisor?.config, - advisorPluginDefaultValues + advisorPluginDefaultValues, + advisorPlugins ), keepAliveWorker: (ortRun.jobConfigs.advisor?.keepAliveWorker && isSuperuser) || @@ -265,7 +266,8 @@ export function defaultValues( ), config: mergePluginConfigs( ortRun.jobConfigs.scanner?.config, - scannerPluginDefaultValues + scannerPluginDefaultValues, + scannerPlugins ), }, evaluator: { @@ -388,4 +390,163 @@ if (import.meta.vitest) { }, }); }); + + it('applies fixed plugin option precedence for advisor and scanner reruns', () => { + const ortRun = { + revision: 'main', + path: '', + jobConfigs: { + advisor: { + advisors: ['OSV'], + config: { + OSV: { + options: { + url: 'https://rerun.example/advisor', + fixedNoDefault: 'legacy-advisor-fixed', + token: 'legacy-advisor-token', + }, + secrets: { + fixedSecret: 'legacy-advisor-secret', + }, + }, + }, + }, + scanner: { + scanners: ['SCANOSS'], + config: { + SCANOSS: { + options: { + url: 'https://rerun.example/scanner', + fixedNoDefault: 'legacy-scanner-fixed', + token: 'legacy-scanner-token', + }, + secrets: { + fixedSecret: 'legacy-scanner-secret', + }, + }, + }, + }, + }, + labels: {}, + } as unknown as OrtRun; + + const advisorPlugins: PreconfiguredPluginDescriptor[] = [ + { + id: 'OSV', + type: 'ADVISOR', + displayName: 'OSV', + summary: 'OSV advisor.', + description: 'OSV advisor.', + options: [ + { + name: 'url', + description: 'Base URL.', + type: 'STRING', + defaultValue: 'https://default.example/advisor', + isFixed: true, + isNullable: false, + isRequired: true, + }, + { + name: 'fixedNoDefault', + description: 'Fixed without default.', + type: 'STRING', + isFixed: true, + isNullable: false, + isRequired: false, + }, + { + name: 'fixedSecret', + description: 'Fixed secret option.', + type: 'SECRET', + isFixed: true, + isNullable: false, + isRequired: false, + }, + { + name: 'token', + description: 'Editable token.', + type: 'STRING', + defaultValue: 'default-token', + isFixed: false, + isNullable: false, + isRequired: false, + }, + ], + }, + ]; + + const scannerPlugins: PreconfiguredPluginDescriptor[] = [ + { + id: 'SCANOSS', + type: 'SCANNER', + displayName: 'SCANOSS', + summary: 'SCANOSS scanner.', + description: 'SCANOSS scanner.', + options: [ + { + name: 'url', + description: 'Base URL.', + type: 'STRING', + defaultValue: 'https://default.example/scanner', + isFixed: true, + isNullable: false, + isRequired: true, + }, + { + name: 'fixedNoDefault', + description: 'Fixed without default.', + type: 'STRING', + isFixed: true, + isNullable: false, + isRequired: false, + }, + { + name: 'fixedSecret', + description: 'Fixed secret option.', + type: 'SECRET', + isFixed: true, + isNullable: false, + isRequired: false, + }, + { + name: 'token', + description: 'Editable token.', + type: 'STRING', + defaultValue: 'default-token', + isFixed: false, + isNullable: false, + isRequired: false, + }, + ], + }, + ]; + + const defaults = defaultValues( + ortRun, + advisorPlugins, + scannerPlugins, + false + ); + + expect(defaults.jobConfigs.advisor.config).toEqual({ + OSV: { + options: { + url: 'https://default.example/advisor', + token: 'legacy-advisor-token', + }, + secrets: {}, + }, + }); + + expect(defaults.jobConfigs.scanner.config).toEqual({ + SCANOSS: { + options: { + url: 'https://default.example/scanner', + token: 'legacy-scanner-token', + }, + secrets: {}, + }, + }); + }); } diff --git a/ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/create-run/-components/plugin-utils.ts b/ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/create-run/-components/plugin-utils.ts index 969dedcc15..fb07beb730 100644 --- a/ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/create-run/-components/plugin-utils.ts +++ b/ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/create-run/-components/plugin-utils.ts @@ -134,20 +134,24 @@ export const createPluginConfigSchema = ( }; /** - * Merge the plugin configs from the last run with the default plugin configs. The configs from the last run take - * precedence. + * Merge the plugin configs from the last run with the default plugin configs. + * For fixed options, values configured via plugin descriptors take precedence. + * For non-fixed options, values from the last run take precedence. */ export function mergePluginConfigs( lastRunConfig: { [p: string]: PluginConfig } | null | undefined, - defaultConfig: Record + defaultConfig: Record, + plugins: PreconfiguredPluginDescriptor[] ): Record { const merged: Record = {}; + const pluginById = new Map(plugins.map((plugin) => [plugin.id, plugin])); for (const pluginId of Object.keys(defaultConfig)) { const defaultPlugin = defaultConfig[pluginId]; const ortPlugin = lastRunConfig?.[pluginId]; + const pluginDescriptor = pluginById.get(pluginId); - merged[pluginId] = { + const mergedPlugin: PluginConfig = { options: { ...(defaultPlugin?.options ?? {}), ...(ortPlugin?.options ?? {}), @@ -157,6 +161,22 @@ export function mergePluginConfigs( ...(ortPlugin?.secrets ?? {}), }, }; + + for (const option of pluginDescriptor?.options ?? []) { + if (!option.isFixed) continue; + + const section = option.type === 'SECRET' ? 'secrets' : 'options'; + const defaultSection = defaultPlugin?.[section] ?? {}; + const defaultValue = defaultSection[option.name]; + + if (defaultValue !== undefined) { + mergedPlugin[section][option.name] = defaultValue; + } else { + delete mergedPlugin[section][option.name]; + } + } + + merged[pluginId] = mergedPlugin; } if (lastRunConfig) { @@ -397,4 +417,77 @@ if (import.meta.vitest) { }, }); }); + + it('mergePluginConfigs enforces fixed option precedence and clears missing fixed defaults', () => { + const plugins: PreconfiguredPluginDescriptor[] = [ + { + id: 'SCANOSS', + type: 'SCANNER', + displayName: 'SCANOSS', + summary: 'A scanner plugin.', + description: 'A scanner plugin.', + options: [ + { + name: 'url', + description: 'The API URL.', + type: 'STRING', + defaultValue: 'https://scanner.example/api', + isFixed: true, + isNullable: false, + isRequired: false, + }, + { + name: 'fixedNoDefault', + description: 'A fixed option without default.', + type: 'STRING', + isFixed: true, + isNullable: false, + isRequired: false, + }, + { + name: 'fixedSecret', + description: 'A fixed secret option.', + type: 'SECRET', + isFixed: true, + isNullable: false, + isRequired: false, + }, + { + name: 'timeout', + description: 'Timeout in seconds.', + type: 'INTEGER', + defaultValue: '30', + isFixed: false, + isNullable: false, + isRequired: false, + }, + ], + }, + ]; + + const merged = mergePluginConfigs( + { + SCANOSS: { + options: { + url: 'https://old.example/api', + fixedNoDefault: 'legacy-value', + timeout: '90', + }, + secrets: { + fixedSecret: 'legacy-secret', + }, + }, + }, + getPluginDefaultValues(plugins), + plugins + ); + + expect(merged.SCANOSS).toEqual({ + options: { + url: 'https://scanner.example/api', + timeout: '90', + }, + secrets: {}, + }); + }); }