Skip to content

[orchestrator-1.9-backport] fix(orchestrator): scope SchemaUpdater replacements#2732

Merged
lokanandaprabhu merged 1 commit intoredhat-developer:orchestrator-1.9-backportfrom
openshift-cherrypick-robot:cherry-pick-2725-to-orchestrator-1.9-backport
Apr 9, 2026
Merged

[orchestrator-1.9-backport] fix(orchestrator): scope SchemaUpdater replacements#2732
lokanandaprabhu merged 1 commit intoredhat-developer:orchestrator-1.9-backportfrom
openshift-cherrypick-robot:cherry-pick-2725-to-orchestrator-1.9-backport

Conversation

@openshift-cherrypick-robot
Copy link
Copy Markdown

This is an automated cherry-pick of #2725

/assign lokanandaprabhu

Limit SchemaUpdater replacements to the originating scope, with robust path
resolution for nested and array schemas, plus tests covering scoping cases.

Made-with: Cursor
@rhdh-qodo-merge
Copy link
Copy Markdown

Review Summary by Qodo

Scope SchemaUpdater replacements to originating step

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Add optional scopeId parameter to SchemaUpdater for scoped updates
• Implement scope resolution logic handling nested objects and arrays
• Limit schema replacements to originating scope only
• Add comprehensive test coverage for scoping scenarios
Diagram
flowchart LR
  A["SchemaUpdater API"] -->|"add scopeId param"| B["OrchestratorFormSchemaUpdater"]
  C["SchemaUpdater Widget"] -->|"pass props.id"| D["updateSchema call"]
  E["Scope Resolution"] -->|"parse scopeId"| F["getScopePathFromId"]
  F -->|"resolve path"| G["getScopedProperties"]
  G -->|"limit updates"| H["deepSearchAndFirstReplace"]
  H -->|"scoped chunks"| I["Updated Schema"]
Loading

Grey Divider

File Changes

1. workspaces/orchestrator/plugins/orchestrator-form-api/src/api.ts ✨ Enhancement +1/-0

Add scopeId parameter to SchemaUpdater type

• Add optional scopeId parameter to OrchestratorFormSchemaUpdater type
• Enables scoped schema updates from form widgets

workspaces/orchestrator/plugins/orchestrator-form-api/src/api.ts


2. workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts 🐞 Bug fix +142/-12

Implement scope resolution for schema updates

• Implement getScopePathFromId function to parse and resolve scope identifiers
• Implement getScopedProperties function to navigate schema to scoped location
• Support both underscore and dot notation for scope path separators
• Handle nested objects and array items in scope resolution
• Prefer longest matching keys to avoid ambiguous matches
• Replace structuredClone with cloneDeep for consistency
• Update getSchemaUpdater to accept and use scopeId parameter

workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts


3. workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.test.ts 🧪 Tests +260/-0

Add comprehensive SchemaUpdater scoping tests

• Add test for scoped updates with multiple root-level steps
• Add test for nested object scoping
• Add test for dot-notation scope identifiers
• Add test for array items scope resolution
• Add test for longest key matching preference

workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.test.ts


View more (3)
4. workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx ✨ Enhancement +1/-1

Pass widget id to schema updater

• Pass props.id as scopeId parameter to updateSchema call
• Enables schema updates to be scoped to the originating widget

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx


5. workspaces/orchestrator/.changeset/schema-updater-scope-fix.md 📝 Documentation +7/-0

Add changeset for schema updater scope fix

• Document patch version bump for orchestrator plugin
• Document patch version bump for orchestrator-form-api plugin
• Document patch version bump for orchestrator-form-widgets plugin
• Describe scope improvement for SchemaUpdater replacements

workspaces/orchestrator/.changeset/schema-updater-scope-fix.md


6. workspaces/orchestrator/plugins/orchestrator-form-api/report.api.md 📝 Documentation +1/-1

Update API report for SchemaUpdater

• Update API report to reflect new optional scopeId parameter

workspaces/orchestrator/plugins/orchestrator-form-api/report.api.md


Grey Divider

Qodo Logo

@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge bot commented Apr 9, 2026

Code Review by Qodo

🐞 Bugs (3)   📘 Rule violations (0)   📎 Requirement gaps (0)   🎨 UX Issues (0)
🐞\ ≡ Correctness (2) ⚙ Maintainability (1)

Grey Divider


Action required

1. Scope failure updates root 🐞
Description
When scopeId cannot be resolved, getScopedProperties() falls back to schema.properties (root),
so getSchemaUpdater() still applies replacements globally even though a scopeId was provided,
potentially modifying other steps/branches of the form schema.
Code

workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[R99-151]

+  const scopePath = getScopePathFromId(schema, scopeId);
+  if (!scopePath || scopePath.length === 0) {
+    if (scopeId) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `SchemaUpdater: Failed to resolve scopeId "${scopeId}", falling back to root`,
+      );
+    }
+    return schema.properties;
+  }
+
+  // Scope to the parent object of the SchemaUpdater field
+  const parentScope = scopePath.slice(0, -1);
+  if (parentScope.length === 0) {
+    return schema.properties;
+  }
+
+  let current: JSONSchema7 | undefined = schema;
+  for (const key of parentScope) {
+    if (
+      current?.type === 'array' &&
+      current.items &&
+      typeof current.items === 'object'
+    ) {
+      current = current.items as JSONSchema7;
+    }
+
+    const properties = current?.properties;
+    if (!properties || typeof properties !== 'object') {
+      return undefined;
+    }
+
+    const next = properties[key];
+    if (!next || typeof next !== 'object') {
+      return undefined;
+    }
+    current = next as JSONSchema7;
+  }
+
+  if (
+    current?.type === 'array' &&
+    current.items &&
+    typeof current.items === 'object'
+  ) {
+    return (current.items as JSONSchema7).properties;
+  }
+
+  return current?.properties;
+};
+
// Stops searching and replacing after first hit
const deepSearchAndFirstReplace = (
  schemaProperties: JSONSchema7['properties'],
Evidence
getScopedProperties() explicitly logs a warning and returns the root schema.properties when it
can’t resolve the scopeId. getSchemaUpdater() then uses those properties (or falls back to root
again via ?? newSchema.properties) as the target for deepSearchAndFirstReplace, which performs
replacements beyond the originating SchemaUpdater’s scope.

workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[99-108]
workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[190-196]
workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx[74-118]

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

### Issue description
When a `scopeId` is provided but can’t be resolved, the code falls back to the schema root and performs replacements globally. This can reintroduce cross-step/incorrect updates.

### Issue Context
This PR’s goal is to scope SchemaUpdater replacements; falling back to root when a scopeId is present undermines that goal.

### Fix Focus Areas
- workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[91-147]
- workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[181-201]

### Suggested change
- If `scopeId` is provided and `getScopePathFromId()` (or `getScopedProperties()`) returns `undefined`/empty, **do not** apply updates (return early) instead of falling back to root.
- Keep the current root behavior only when `scopeId` is not provided (backward-compatible).

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



Remediation recommended

2. Array indices not supported 🐞
Description
getScopePathFromId() recurses into array.items without consuming any id segment, so ids that
include array indices (e.g. ..._items_0_schemaUpdater) won’t match and are likely to trigger
unresolved-scope behavior.
Code

workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[R44-86]

+  const separators = ['_', '.'];
+  const matchPath = (
+    node: JSONSchema7,
+    remaining: string,
+  ): string[] | undefined => {
+    if (!remaining) {
+      return undefined;
+    }
+
+    if (node.type === 'array' && node.items && typeof node.items === 'object') {
+      return matchPath(node.items as JSONSchema7, remaining);
+    }
+
+    if (!node.properties) {
+      return undefined;
+    }
+
+    const keys = Object.keys(node.properties).sort(
+      (a, b) => b.length - a.length,
+    );
+    for (const key of keys) {
+      for (const separator of separators) {
+        if (remaining === key) {
+          return [key];
+        }
+
+        if (remaining.startsWith(`${key}${separator}`)) {
+          const next = node.properties[key] as JSONSchema7;
+          if (!next || typeof next !== 'object') {
+            continue;
+          }
+
+          const rest = remaining.slice(key.length + separator.length);
+          const childPath = matchPath(next, rest);
+          if (childPath) {
+            return [key, ...childPath];
+          }
+        }
+      }
+    }
+
+    return undefined;
+  };
Evidence
In matchPath(), when the current node is an array it immediately recurses into node.items with
the same remaining string. If the next segment in remaining represents an index, there is no
code path that removes/skips it, so subsequent property-key matching will fail.

workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[44-55]
workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[61-85]

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

### Issue description
Scope path resolution doesn’t handle array index segments in `scopeId`, so scoping can fail for SchemaUpdater widgets rendered under array items.

### Issue Context
`matchPath()` currently treats arrays as transparent (`array` -> `items`) but doesn’t strip an index segment from the id path before descending.

### Fix Focus Areas
- workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[44-86]

### Suggested change
- In the `node.type === 'array'` branch, detect and skip a leading numeric segment from `remaining` (for both `_` and `.` separators), e.g. consume `^\d+[_\.]` before recursing.
- Add/adjust unit tests to cover an indexed id (see existing test file).

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



Advisory comments

3. Missing indexed-array test 🐞
Description
The new unit test for array scoping uses root_step_items_schemaUpdater and doesn’t cover an id
containing an array index, so it won’t catch the array-index scoping failure mode.
Code

workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.test.ts[R160-208]

+  it('resolves scope through array items', () => {
+    const schema = {
+      type: 'object',
+      properties: {
+        step: {
+          type: 'object',
+          properties: {
+            items: {
+              type: 'array',
+              items: {
+                type: 'object',
+                properties: {
+                  schemaUpdater: { type: 'string' },
+                  placeholderTwo: {
+                    type: 'string',
+                    title: 'Array placeholder',
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    } as JSONSchema7;
+
+    const setSchema = jest.fn();
+    const updateSchema = getSchemaUpdater(schema, setSchema);
+
+    updateSchema(
+      {
+        placeholderTwo: {
+          type: 'string',
+          title: 'Array placeholder (updated)',
+        },
+      },
+      'root_step_items_schemaUpdater',
+    );
+
+    expect(setSchema).toHaveBeenCalledTimes(1);
+    const updatedSchema = setSchema.mock.calls[0][0] as JSONSchema7;
+    const arrayProps = (
+      (updatedSchema.properties?.step as JSONSchema7).properties
+        ?.items as JSONSchema7
+    ).items as JSONSchema7;
+    expect(arrayProps.properties?.placeholderTwo).toEqual({
+      type: 'string',
+      title: 'Array placeholder (updated)',
+    });
+  });
Evidence
The array test validates scoping through an array type but uses a scopeId without any index
component, leaving the indexed-id behavior untested.

workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.test.ts[160-208]

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

### Issue description
Test coverage doesn’t include scopeIds that contain array indices.

### Issue Context
There is already an array-focused test; it can be extended or complemented with an indexed-id case.

### Fix Focus Areas
- workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.test.ts[160-208]

### Suggested change
- Add a test that calls `updateSchema(..., 'root_step_items_0_schemaUpdater')` (and/or dot-notation variant) and asserts only the intended array-item subtree is updated.
- This should fail on the current implementation and pass after array-index handling is added.

ⓘ 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

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 9, 2026

Comment on lines +99 to 151
const scopePath = getScopePathFromId(schema, scopeId);
if (!scopePath || scopePath.length === 0) {
if (scopeId) {
// eslint-disable-next-line no-console
console.warn(
`SchemaUpdater: Failed to resolve scopeId "${scopeId}", falling back to root`,
);
}
return schema.properties;
}

// Scope to the parent object of the SchemaUpdater field
const parentScope = scopePath.slice(0, -1);
if (parentScope.length === 0) {
return schema.properties;
}

let current: JSONSchema7 | undefined = schema;
for (const key of parentScope) {
if (
current?.type === 'array' &&
current.items &&
typeof current.items === 'object'
) {
current = current.items as JSONSchema7;
}

const properties = current?.properties;
if (!properties || typeof properties !== 'object') {
return undefined;
}

const next = properties[key];
if (!next || typeof next !== 'object') {
return undefined;
}
current = next as JSONSchema7;
}

if (
current?.type === 'array' &&
current.items &&
typeof current.items === 'object'
) {
return (current.items as JSONSchema7).properties;
}

return current?.properties;
};

// Stops searching and replacing after first hit
const deepSearchAndFirstReplace = (
schemaProperties: JSONSchema7['properties'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Scope failure updates root 🐞 Bug ≡ Correctness

When scopeId cannot be resolved, getScopedProperties() falls back to schema.properties (root),
so getSchemaUpdater() still applies replacements globally even though a scopeId was provided,
potentially modifying other steps/branches of the form schema.
Agent Prompt
### Issue description
When a `scopeId` is provided but can’t be resolved, the code falls back to the schema root and performs replacements globally. This can reintroduce cross-step/incorrect updates.

### Issue Context
This PR’s goal is to scope SchemaUpdater replacements; falling back to root when a scopeId is present undermines that goal.

### Fix Focus Areas
- workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[91-147]
- workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/schemaUpdater.ts[181-201]

### Suggested change
- If `scopeId` is provided and `getScopePathFromId()` (or `getScopedProperties()`) returns `undefined`/empty, **do not** apply updates (return early) instead of falling back to root.
- Keep the current root behavior only when `scopeId` is not provided (backward-compatible).

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

Copy link
Copy Markdown
Member

@lokanandaprabhu lokanandaprabhu left a comment

Choose a reason for hiding this comment

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

Tested locally and it is working fine

/lgtm

Screen.Recording.2026-04-09.at.11.56.51.AM.mov

@openshift-ci openshift-ci bot added the lgtm label Apr 9, 2026
@lokanandaprabhu lokanandaprabhu merged commit 3281e3c into redhat-developer:orchestrator-1.9-backport Apr 9, 2026
8 checks passed
lokanandaprabhu added a commit that referenced this pull request Apr 9, 2026
* [orchestrator] Backport orchestrator fixes and enhancements to 1.9 (#2666)

* feat(orchestrator): pre-populate Execute Workflow form from URL query params (#2570)

* prepopulate workflow execution page form from URL query params

Signed-off-by: Karthik <karthik.jk11@gmail.com>

* support enum coercison for case-insenstive match and skip invalid values

* add support for fields that are defined via '$ref'

* add full support for json schema fields

---------

Signed-off-by: Karthik <karthik.jk11@gmail.com>

* fix(orchestrator-form-react): scope async validation to active step (#2602)

* fix(orchestrator-form-react): scope async validation to active step

Limit validate:url requests to the active step during multi-step navigation.

Made-with: Cursor

* fix(orchestrator-form-react): keep full formData for async validation

Pass full formData to template evaluation while scoping uiSchema traversal.

Made-with: Cursor

* fix(orchestrator-form-react): preserve ui:hidden on objects with properties (#2653)

* fix(orchestrator): honor json schema defaults in initial formData (#2654)

* fix(orchestrator): honor json schema defaults

Ensure extractStaticDefaults falls back to JSON Schema defaults when ui:props fetch:response:default is absent so initial formData includes schema defaults.

Made-with: Cursor

* chore(changeset): document schema default fix

Add changeset for orchestrator form defaults update.

Made-with: Cursor

* fix(orchestrator): handle root defaults

Avoid setting an empty key for root schema defaults and add tests to cover root default handling.

Made-with: Cursor

* fix(orchestrator): document and test defaults

Clarify extractStaticDefaults precedence in docs and add test coverage for default handling.

Made-with: Cursor

* chore(orchestrator): update yarn.lock after backport

Made-with: Cursor

---------

Signed-off-by: Karthik <karthik.jk11@gmail.com>
Co-authored-by: Karthik Jeeyar <karthik@redhat.com>

* fix(orchestrator): scope SchemaUpdater replacements (#2732)

Limit SchemaUpdater replacements to the originating scope, with robust path
resolution for nested and array schemas, plus tests covering scoping cases.

Made-with: Cursor

Co-authored-by: Lokananda Prabhu <lprabhu@redhat.com>

---------

Signed-off-by: Karthik <karthik.jk11@gmail.com>
Co-authored-by: Karthik Jeeyar <karthik@redhat.com>
Co-authored-by: OpenShift Cherrypick Robot <openshift-cherrypick-robot@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants