Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Commit ccfd2ac

Browse files
committed
feat(luminork): Change set review endpoint
Adds a new `/review` endpoint to the Luminork API that provides a comprehensive, single-call review of all component changes in a change set. ## Motivation Currently, to review all changes in a change set, clients would need to: 1. List all components 2. Fetch individual component diffs for each one 3. Fetch component metadata for subscription resolution 4. Filter out noise (empty defaults, internal fields, etc.) This requires **N+1 API calls** and complex client-side logic. For workspaces with 100+ components, this is slow and inefficient. ## Solution New endpoint: `GET /v1/w/{workspace_id}/change-sets/{change_set_id}/review` Returns all component diffs with changes in a **single API call**, with: - ✅ Pre-filtered attribute diffs (removes noise) - ✅ Simplified, CLI-friendly format - ✅ Subscription source resolution included - ✅ Summary statistics - ✅ Optional resource code diffs ## Implementation Details ### **Uses Pre-Computed MVs from Frigg** The endpoint leverages existing `ComponentDiff` materialized views: - Fetches from Frigg's object store (fast!) - Returns **202 Accepted** if MVs not ready (triggers edda rebuild) - No expensive on-demand diff calculation ### **Smart Filtering (Same as Web UI)** Applies the same filtering logic as `app/web/src/newhotness/Review.vue`: **Excluded diffs:** - Internal fields: `/si/type`, `/si/color` - Identical old/new values (can occur on schema upgrades) - Empty schema defaults: `{}`, `[]`, `null`, `""`, `0` - Object field placeholders at top-level paths **Diff status recalculation:** - If filtering removes all attribute diffs → `Modified` becomes `None` - Only returns components with meaningful changes ### **Simplified Response Format** Instead of the complex MV format: ```json { "$source": { "component": "01HZZZ...", "path": "/domain/region" }, "$value": "us-east-1" } ``` Returns CLI-friendly format: ```json { "changeType": "added", "newValue": "us-east-1", "newSourceType": "subscription", "newSourceComponentName": "region-1", "newSourcePath": "/domain/region" } ``` ## Design Decisions ### Why Not Parallel MV Fetching? Uses sequential fetching with early filtering instead of parallel. This is fine because: - MVs are pre-computed and cached (fast to fetch) - We filter by diff_status early (skip unchanged components) - Simpler error handling - Follows existing patterns in the codebase ### Why Server-Side Filtering? - **Consistency**: Web and CLI show the same filtered view - **Simplicity**: CLI doesn't need to reimplement filtering logic - **Single source of truth**: Changes to filtering happen in one place - **Better UX**: Users see clean, curated diffs ### Why Simplified Format? The raw MV format with `$source` and `$value` is: - Hard to parse - Verbose - Requires understanding SI's internal data model The simplified format is: - Self-documenting - Easy to render in CLI - Clear separation of value vs source ## Example Responses ``` { "components": [ { "componentId": "01KF272RATCMXECD5GHD7291B1", "componentName": "test", "schemaName": "AWS Credential", "diffStatus": "Added", "attributeDiffs": { "/si/name": { "changeType": "added", "newValue": "test", "newSourceType": "value" }, "/secrets/AWS Credential": { "changeType": "added", "newValue": "fb24d5e22162989a269899c3e3d27654", "newSourceType": "value" } } }, { "componentId": "01KF28M419BZXWWXN4EKQPNFKM", "componentName": "si-0953", "schemaName": "Region", "diffStatus": "Added", "attributeDiffs": { "/si/name": { "changeType": "added", "newValue": "si-0953", "newSourceType": "value" }, "/domain/region": { "changeType": "added", "newValue": "us-east-1", "newSourceType": "value" }, "/secrets/credential": { "changeType": "added", "newValue": "fb24d5e22162989a269899c3e3d27654", "newSourceType": "subscription", "newSourceComponentName": "test", "newSourceComponentId": "01KF272RATCMXECD5GHD7291B1", "newSourcePath": "/secrets/AWS Credential" } } } ], "summary": { "totalComponents": 2, "added": 2, "modified": 0, "removed": 0 } } ``` ``` { "components": [ { "componentId": "01KF28M419BZXWWXN4EKQPNFKM", "componentName": "si-0953", "schemaName": "Region", "diffStatus": "Modified", "attributeDiffs": { "/domain/region": { "changeType": "modified", "newValue": "us-east-2", "oldValue": "us-east-1", "newSourceType": "value", "oldSourceType": "value" } } } ], "summary": { "totalComponents": 1, "added": 0, "modified": 1, "removed": 0 } } ```
1 parent d57a38e commit ccfd2ac

17 files changed

Lines changed: 2005 additions & 1 deletion

generated-sdks/python/system_initiative_api_client/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@
5353
"AttributeArgumentBindingRequest",
5454
"BuildingResponseV1",
5555
"CancelActionV1Response",
56+
"ChangeSetReviewV1Response",
5657
"ChangeSetViewV1",
5758
"ComponentDetailsV1",
5859
"ComponentPropKey",
5960
"ComponentPropViewV1",
6061
"ComponentReference",
6162
"ComponentReferenceOneOf",
6263
"ComponentReferenceOneOf1",
64+
"ComponentReviewV1",
6365
"ComponentSearchResult",
6466
"ComponentSearchResultSchema",
6567
"ComponentV1RequestPath",
@@ -153,6 +155,7 @@
153155
"RequestApprovalChangeSetV1Response",
154156
"RestoreComponentV1Response",
155157
"RetryActionV1Response",
158+
"ReviewSummaryV1",
156159
"SchemaResponse",
157160
"SchemaV1RequestPath",
158161
"SchemaVariantFunc",
@@ -171,6 +174,7 @@
171174
"SecretFormDataV1",
172175
"SecretPropKey",
173176
"SecretV1",
177+
"SimplifiedAttributeDiffV1",
174178
"SourceViewV1",
175179
"SubscriptionRelationshipV1",
176180
"SystemStatusResponse",
@@ -235,13 +239,15 @@
235239
from system_initiative_api_client.models.attribute_argument_binding_request import AttributeArgumentBindingRequest as AttributeArgumentBindingRequest
236240
from system_initiative_api_client.models.building_response_v1 import BuildingResponseV1 as BuildingResponseV1
237241
from system_initiative_api_client.models.cancel_action_v1_response import CancelActionV1Response as CancelActionV1Response
242+
from system_initiative_api_client.models.change_set_review_v1_response import ChangeSetReviewV1Response as ChangeSetReviewV1Response
238243
from system_initiative_api_client.models.change_set_view_v1 import ChangeSetViewV1 as ChangeSetViewV1
239244
from system_initiative_api_client.models.component_details_v1 import ComponentDetailsV1 as ComponentDetailsV1
240245
from system_initiative_api_client.models.component_prop_key import ComponentPropKey as ComponentPropKey
241246
from system_initiative_api_client.models.component_prop_view_v1 import ComponentPropViewV1 as ComponentPropViewV1
242247
from system_initiative_api_client.models.component_reference import ComponentReference as ComponentReference
243248
from system_initiative_api_client.models.component_reference_one_of import ComponentReferenceOneOf as ComponentReferenceOneOf
244249
from system_initiative_api_client.models.component_reference_one_of1 import ComponentReferenceOneOf1 as ComponentReferenceOneOf1
250+
from system_initiative_api_client.models.component_review_v1 import ComponentReviewV1 as ComponentReviewV1
245251
from system_initiative_api_client.models.component_search_result import ComponentSearchResult as ComponentSearchResult
246252
from system_initiative_api_client.models.component_search_result_schema import ComponentSearchResultSchema as ComponentSearchResultSchema
247253
from system_initiative_api_client.models.component_v1_request_path import ComponentV1RequestPath as ComponentV1RequestPath
@@ -335,6 +341,7 @@
335341
from system_initiative_api_client.models.request_approval_change_set_v1_response import RequestApprovalChangeSetV1Response as RequestApprovalChangeSetV1Response
336342
from system_initiative_api_client.models.restore_component_v1_response import RestoreComponentV1Response as RestoreComponentV1Response
337343
from system_initiative_api_client.models.retry_action_v1_response import RetryActionV1Response as RetryActionV1Response
344+
from system_initiative_api_client.models.review_summary_v1 import ReviewSummaryV1 as ReviewSummaryV1
338345
from system_initiative_api_client.models.schema_response import SchemaResponse as SchemaResponse
339346
from system_initiative_api_client.models.schema_v1_request_path import SchemaV1RequestPath as SchemaV1RequestPath
340347
from system_initiative_api_client.models.schema_variant_func import SchemaVariantFunc as SchemaVariantFunc
@@ -353,6 +360,7 @@
353360
from system_initiative_api_client.models.secret_form_data_v1 import SecretFormDataV1 as SecretFormDataV1
354361
from system_initiative_api_client.models.secret_prop_key import SecretPropKey as SecretPropKey
355362
from system_initiative_api_client.models.secret_v1 import SecretV1 as SecretV1
363+
from system_initiative_api_client.models.simplified_attribute_diff_v1 import SimplifiedAttributeDiffV1 as SimplifiedAttributeDiffV1
356364
from system_initiative_api_client.models.source_view_v1 import SourceViewV1 as SourceViewV1
357365
from system_initiative_api_client.models.subscription_relationship_v1 import SubscriptionRelationshipV1 as SubscriptionRelationshipV1
358366
from system_initiative_api_client.models.system_status_response import SystemStatusResponse as SystemStatusResponse

generated-sdks/python/system_initiative_api_client/api/change_sets_api.py

Lines changed: 307 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
from typing import Any, Dict, List, Optional, Tuple, Union
1717
from typing_extensions import Annotated
1818

19-
from pydantic import Field, StrictStr
19+
from pydantic import Field, StrictBool, StrictStr
20+
from typing import Optional
2021
from typing_extensions import Annotated
22+
from system_initiative_api_client.models.change_set_review_v1_response import ChangeSetReviewV1Response
2123
from system_initiative_api_client.models.create_change_set_v1_request import CreateChangeSetV1Request
2224
from system_initiative_api_client.models.create_change_set_v1_response import CreateChangeSetV1Response
2325
from system_initiative_api_client.models.delete_change_set_v1_response import DeleteChangeSetV1Response
@@ -2260,3 +2262,307 @@ def _request_approval_serialize(
22602262
)
22612263

22622264

2265+
2266+
2267+
@validate_call
2268+
def review_change_set(
2269+
self,
2270+
workspace_id: Annotated[StrictStr, Field(description="Workspace identifier")],
2271+
change_set_id: Annotated[StrictStr, Field(description="Change Set identifier")],
2272+
include_resource_diff: Annotated[Optional[StrictBool], Field(description="Include resource code diffs (CloudFormation/Terraform)")] = None,
2273+
_request_timeout: Union[
2274+
None,
2275+
Annotated[StrictFloat, Field(gt=0)],
2276+
Tuple[
2277+
Annotated[StrictFloat, Field(gt=0)],
2278+
Annotated[StrictFloat, Field(gt=0)]
2279+
]
2280+
] = None,
2281+
_request_auth: Optional[Dict[StrictStr, Any]] = None,
2282+
_content_type: Optional[StrictStr] = None,
2283+
_headers: Optional[Dict[StrictStr, Any]] = None,
2284+
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
2285+
) -> ChangeSetReviewV1Response:
2286+
"""Get a comprehensive review of all changes in a change set
2287+
2288+
Returns all components with diffs in a single call. Includes component lookup data for resolving subscription sources.
2289+
2290+
:param workspace_id: Workspace identifier (required)
2291+
:type workspace_id: str
2292+
:param change_set_id: Change Set identifier (required)
2293+
:type change_set_id: str
2294+
:param include_resource_diff: Include resource code diffs (CloudFormation/Terraform)
2295+
:type include_resource_diff: bool
2296+
:param _request_timeout: timeout setting for this request. If one
2297+
number provided, it will be total request
2298+
timeout. It can also be a pair (tuple) of
2299+
(connection, read) timeouts.
2300+
:type _request_timeout: int, tuple(int, int), optional
2301+
:param _request_auth: set to override the auth_settings for an a single
2302+
request; this effectively ignores the
2303+
authentication in the spec for a single request.
2304+
:type _request_auth: dict, optional
2305+
:param _content_type: force content-type for the request.
2306+
:type _content_type: str, Optional
2307+
:param _headers: set to override the headers for a single
2308+
request; this effectively ignores the headers
2309+
in the spec for a single request.
2310+
:type _headers: dict, optional
2311+
:param _host_index: set to override the host_index for a single
2312+
request; this effectively ignores the host_index
2313+
in the spec for a single request.
2314+
:type _host_index: int, optional
2315+
:return: Returns the result object.
2316+
""" # noqa: E501
2317+
2318+
_param = self._review_change_set_serialize(
2319+
workspace_id=workspace_id,
2320+
change_set_id=change_set_id,
2321+
include_resource_diff=include_resource_diff,
2322+
_request_auth=_request_auth,
2323+
_content_type=_content_type,
2324+
_headers=_headers,
2325+
_host_index=_host_index
2326+
)
2327+
2328+
_response_types_map: Dict[str, Optional[str]] = {
2329+
'200': "ChangeSetReviewV1Response",
2330+
'202': "BuildingResponseV1",
2331+
'401': None,
2332+
'404': None,
2333+
'500': None,
2334+
}
2335+
response_data = self.api_client.call_api(
2336+
*_param,
2337+
_request_timeout=_request_timeout
2338+
)
2339+
response_data.read()
2340+
return self.api_client.response_deserialize(
2341+
response_data=response_data,
2342+
response_types_map=_response_types_map,
2343+
).data
2344+
2345+
2346+
@validate_call
2347+
def review_change_set_with_http_info(
2348+
self,
2349+
workspace_id: Annotated[StrictStr, Field(description="Workspace identifier")],
2350+
change_set_id: Annotated[StrictStr, Field(description="Change Set identifier")],
2351+
include_resource_diff: Annotated[Optional[StrictBool], Field(description="Include resource code diffs (CloudFormation/Terraform)")] = None,
2352+
_request_timeout: Union[
2353+
None,
2354+
Annotated[StrictFloat, Field(gt=0)],
2355+
Tuple[
2356+
Annotated[StrictFloat, Field(gt=0)],
2357+
Annotated[StrictFloat, Field(gt=0)]
2358+
]
2359+
] = None,
2360+
_request_auth: Optional[Dict[StrictStr, Any]] = None,
2361+
_content_type: Optional[StrictStr] = None,
2362+
_headers: Optional[Dict[StrictStr, Any]] = None,
2363+
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
2364+
) -> ApiResponse[ChangeSetReviewV1Response]:
2365+
"""Get a comprehensive review of all changes in a change set
2366+
2367+
Returns all components with diffs in a single call. Includes component lookup data for resolving subscription sources.
2368+
2369+
:param workspace_id: Workspace identifier (required)
2370+
:type workspace_id: str
2371+
:param change_set_id: Change Set identifier (required)
2372+
:type change_set_id: str
2373+
:param include_resource_diff: Include resource code diffs (CloudFormation/Terraform)
2374+
:type include_resource_diff: bool
2375+
:param _request_timeout: timeout setting for this request. If one
2376+
number provided, it will be total request
2377+
timeout. It can also be a pair (tuple) of
2378+
(connection, read) timeouts.
2379+
:type _request_timeout: int, tuple(int, int), optional
2380+
:param _request_auth: set to override the auth_settings for an a single
2381+
request; this effectively ignores the
2382+
authentication in the spec for a single request.
2383+
:type _request_auth: dict, optional
2384+
:param _content_type: force content-type for the request.
2385+
:type _content_type: str, Optional
2386+
:param _headers: set to override the headers for a single
2387+
request; this effectively ignores the headers
2388+
in the spec for a single request.
2389+
:type _headers: dict, optional
2390+
:param _host_index: set to override the host_index for a single
2391+
request; this effectively ignores the host_index
2392+
in the spec for a single request.
2393+
:type _host_index: int, optional
2394+
:return: Returns the result object.
2395+
""" # noqa: E501
2396+
2397+
_param = self._review_change_set_serialize(
2398+
workspace_id=workspace_id,
2399+
change_set_id=change_set_id,
2400+
include_resource_diff=include_resource_diff,
2401+
_request_auth=_request_auth,
2402+
_content_type=_content_type,
2403+
_headers=_headers,
2404+
_host_index=_host_index
2405+
)
2406+
2407+
_response_types_map: Dict[str, Optional[str]] = {
2408+
'200': "ChangeSetReviewV1Response",
2409+
'202': "BuildingResponseV1",
2410+
'401': None,
2411+
'404': None,
2412+
'500': None,
2413+
}
2414+
response_data = self.api_client.call_api(
2415+
*_param,
2416+
_request_timeout=_request_timeout
2417+
)
2418+
response_data.read()
2419+
return self.api_client.response_deserialize(
2420+
response_data=response_data,
2421+
response_types_map=_response_types_map,
2422+
)
2423+
2424+
2425+
@validate_call
2426+
def review_change_set_without_preload_content(
2427+
self,
2428+
workspace_id: Annotated[StrictStr, Field(description="Workspace identifier")],
2429+
change_set_id: Annotated[StrictStr, Field(description="Change Set identifier")],
2430+
include_resource_diff: Annotated[Optional[StrictBool], Field(description="Include resource code diffs (CloudFormation/Terraform)")] = None,
2431+
_request_timeout: Union[
2432+
None,
2433+
Annotated[StrictFloat, Field(gt=0)],
2434+
Tuple[
2435+
Annotated[StrictFloat, Field(gt=0)],
2436+
Annotated[StrictFloat, Field(gt=0)]
2437+
]
2438+
] = None,
2439+
_request_auth: Optional[Dict[StrictStr, Any]] = None,
2440+
_content_type: Optional[StrictStr] = None,
2441+
_headers: Optional[Dict[StrictStr, Any]] = None,
2442+
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
2443+
) -> RESTResponseType:
2444+
"""Get a comprehensive review of all changes in a change set
2445+
2446+
Returns all components with diffs in a single call. Includes component lookup data for resolving subscription sources.
2447+
2448+
:param workspace_id: Workspace identifier (required)
2449+
:type workspace_id: str
2450+
:param change_set_id: Change Set identifier (required)
2451+
:type change_set_id: str
2452+
:param include_resource_diff: Include resource code diffs (CloudFormation/Terraform)
2453+
:type include_resource_diff: bool
2454+
:param _request_timeout: timeout setting for this request. If one
2455+
number provided, it will be total request
2456+
timeout. It can also be a pair (tuple) of
2457+
(connection, read) timeouts.
2458+
:type _request_timeout: int, tuple(int, int), optional
2459+
:param _request_auth: set to override the auth_settings for an a single
2460+
request; this effectively ignores the
2461+
authentication in the spec for a single request.
2462+
:type _request_auth: dict, optional
2463+
:param _content_type: force content-type for the request.
2464+
:type _content_type: str, Optional
2465+
:param _headers: set to override the headers for a single
2466+
request; this effectively ignores the headers
2467+
in the spec for a single request.
2468+
:type _headers: dict, optional
2469+
:param _host_index: set to override the host_index for a single
2470+
request; this effectively ignores the host_index
2471+
in the spec for a single request.
2472+
:type _host_index: int, optional
2473+
:return: Returns the result object.
2474+
""" # noqa: E501
2475+
2476+
_param = self._review_change_set_serialize(
2477+
workspace_id=workspace_id,
2478+
change_set_id=change_set_id,
2479+
include_resource_diff=include_resource_diff,
2480+
_request_auth=_request_auth,
2481+
_content_type=_content_type,
2482+
_headers=_headers,
2483+
_host_index=_host_index
2484+
)
2485+
2486+
_response_types_map: Dict[str, Optional[str]] = {
2487+
'200': "ChangeSetReviewV1Response",
2488+
'202': "BuildingResponseV1",
2489+
'401': None,
2490+
'404': None,
2491+
'500': None,
2492+
}
2493+
response_data = self.api_client.call_api(
2494+
*_param,
2495+
_request_timeout=_request_timeout
2496+
)
2497+
return response_data.response
2498+
2499+
2500+
def _review_change_set_serialize(
2501+
self,
2502+
workspace_id,
2503+
change_set_id,
2504+
include_resource_diff,
2505+
_request_auth,
2506+
_content_type,
2507+
_headers,
2508+
_host_index,
2509+
) -> RequestSerialized:
2510+
2511+
_host = None
2512+
2513+
_collection_formats: Dict[str, str] = {
2514+
}
2515+
2516+
_path_params: Dict[str, str] = {}
2517+
_query_params: List[Tuple[str, str]] = []
2518+
_header_params: Dict[str, Optional[str]] = _headers or {}
2519+
_form_params: List[Tuple[str, str]] = []
2520+
_files: Dict[
2521+
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
2522+
] = {}
2523+
_body_params: Optional[bytes] = None
2524+
2525+
# process the path parameters
2526+
if workspace_id is not None:
2527+
_path_params['workspace_id'] = workspace_id
2528+
if change_set_id is not None:
2529+
_path_params['change_set_id'] = change_set_id
2530+
# process the query parameters
2531+
if include_resource_diff is not None:
2532+
2533+
_query_params.append(('includeResourceDiff', include_resource_diff))
2534+
2535+
# process the header parameters
2536+
# process the form parameters
2537+
# process the body parameter
2538+
2539+
2540+
# set the HTTP header `Accept`
2541+
if 'Accept' not in _header_params:
2542+
_header_params['Accept'] = self.api_client.select_header_accept(
2543+
[
2544+
'application/json'
2545+
]
2546+
)
2547+
2548+
2549+
# authentication setting
2550+
_auth_settings: List[str] = [
2551+
]
2552+
2553+
return self.api_client.param_serialize(
2554+
method='GET',
2555+
resource_path='/v1/w/{workspace_id}/change-sets/{change_set_id}/review',
2556+
path_params=_path_params,
2557+
query_params=_query_params,
2558+
header_params=_header_params,
2559+
body=_body_params,
2560+
post_params=_form_params,
2561+
files=_files,
2562+
auth_settings=_auth_settings,
2563+
collection_formats=_collection_formats,
2564+
_host=_host,
2565+
_request_auth=_request_auth
2566+
)
2567+
2568+

0 commit comments

Comments
 (0)