Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions apps/api/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -7492,6 +7492,130 @@
"summary": "Get the desired release for a release target"
}
},
"/v1/workspaces/{workspaceId}/release-targets/{releaseTargetKey}/eligible-versions": {
"post": {
"description": "Returns deployment versions that currently pass every policy rule for this release target. An optional CEL filter narrows the result; pagination is applied to the filtered set. Use the \"version\" variable in the CEL expression to access version properties.",
"operationId": "listEligibleVersionsForReleaseTarget",
"parameters": [
{
"description": "ID of the workspace",
"in": "path",
"name": "workspaceId",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Key of the release target",
"in": "path",
"name": "releaseTargetKey",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Maximum number of items to return",
"in": "query",
"name": "limit",
"required": false,
"schema": {
"default": 50,
"maximum": 1000,
"minimum": 1,
"type": "integer"
}
},
{
"description": "Number of items to skip",
"in": "query",
"name": "offset",
"required": false,
"schema": {
"default": 0,
"minimum": 0,
"type": "integer"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"filter": {
"description": "CEL expression to filter eligible versions. Defaults to \"true\" (all eligible versions).",
"type": "string"
}
},
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/DeploymentVersion"
},
"type": "array"
},
"limit": {
"description": "Maximum number of items returned",
"type": "integer"
},
"offset": {
"description": "Number of items skipped",
"type": "integer"
},
"total": {
"description": "Total number of items available",
"type": "integer"
}
},
"required": [
"items",
"total",
"limit",
"offset"
],
"type": "object"
}
}
},
"description": "Eligible versions for the release target"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Invalid request"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "Resource not found"
}
},
"summary": "List versions eligible for a release target"
}
},
"/v1/workspaces/{workspaceId}/release-targets/{releaseTargetKey}/jobs": {
"get": {
"description": "Returns a list of jobs for a release target {releaseTargetKey}.",
Expand Down
33 changes: 33 additions & 0 deletions apps/api/openapi/paths/release-targets.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,39 @@ local openapi = import '../lib/openapi.libsonnet';
},
},

'/v1/workspaces/{workspaceId}/release-targets/{releaseTargetKey}/eligible-versions': {
post: {
summary: 'List versions eligible for a release target',
operationId: 'listEligibleVersionsForReleaseTarget',
description: 'Returns deployment versions that currently pass every policy rule for this release target. An optional CEL filter narrows the result; pagination is applied to the filtered set. Use the "version" variable in the CEL expression to access version properties.',
parameters: [
openapi.workspaceIdParam(),
openapi.releaseTargetKeyParam(),
openapi.limitParam(),
openapi.offsetParam(),
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'CEL expression to filter eligible versions. Defaults to "true" (all eligible versions).',
},
},
},
},
},
},
responses: openapi.paginatedResponse(openapi.schemaRef('DeploymentVersion'), 'Eligible versions for the release target')
+ openapi.notFoundResponse()
+ openapi.badRequestResponse(),
},
},

'/v1/workspaces/{workspaceId}/release-targets/resource-preview': {
post: {
summary: 'Preview release targets for a resource',
Expand Down
34 changes: 33 additions & 1 deletion apps/api/src/routes/v1/workspaces/release-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,34 @@ const getReleaseTargetState: AsyncTypedHandler<
res.status(200).json(data);
};

const listEligibleVersionsForReleaseTarget: AsyncTypedHandler<
"/v1/workspaces/{workspaceId}/release-targets/{releaseTargetKey}/eligible-versions",
"post"
> = async (req, res) => {
const { workspaceId, releaseTargetKey } = req.params;
const { limit, offset } = req.query;
const { filter } = req.body;

const { data, error, response } = await getClientFor(workspaceId).POST(
"/v1/workspaces/{workspaceId}/release-targets/{releaseTargetKey}/eligible-versions",
{
params: {
path: { workspaceId, releaseTargetKey },
query: { limit, offset },
},
body: { filter },
},
Comment on lines +245 to +255
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard optional request body before destructuring.

Line 245 can throw a runtime TypeError when clients omit the request body. This endpoint supports optional filtering, so body access should be null-safe.

Suggested fix
-  const { filter } = req.body;
+  const filter = req.body?.filter;
...
-      body: { filter },
+      ...(filter !== undefined ? { body: { filter } } : {}),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { filter } = req.body;
const { data, error, response } = await getClientFor(workspaceId).POST(
"/v1/workspaces/{workspaceId}/release-targets/{releaseTargetKey}/eligible-versions",
{
params: {
path: { workspaceId, releaseTargetKey },
query: { limit, offset },
},
body: { filter },
},
const filter = req.body?.filter;
const { data, error, response } = await getClientFor(workspaceId).POST(
"/v1/workspaces/{workspaceId}/release-targets/{releaseTargetKey}/eligible-versions",
{
params: {
path: { workspaceId, releaseTargetKey },
query: { limit, offset },
},
...(filter !== undefined ? { body: { filter } } : {}),
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/v1/workspaces/release-targets.ts` around lines 245 - 255,
The code destructures filter from req.body which can be undefined and cause a
TypeError; update the destructuring to guard against a missing body (e.g., use a
fallback like req.body || {} or optional chaining) so that const { filter } =
... never throws, and keep the rest of the POST call to getClientFor(...).POST
unchanged.

);

if (error != null)
throw new ApiError(
error.error ?? "Failed to list eligible versions",
response.status >= 400 && response.status < 500 ? response.status : 502,
);

res.status(200).json(data);
};

const getReleaseTargetStates: AsyncTypedHandler<
"/v1/workspaces/{workspaceId}/release-targets/state",
"post"
Expand Down Expand Up @@ -436,7 +464,11 @@ const previewReleaseTargetsForResource: AsyncTypedHandler<
const releaseTargetKeyRouter = Router({ mergeParams: true })
.get("/jobs", asyncHandler(getReleaseTargetJobs))
.get("/desired-release", asyncHandler(getReleaseTargetDesiredRelease))
.get("/state", asyncHandler(getReleaseTargetState));
.get("/state", asyncHandler(getReleaseTargetState))
.post(
"/eligible-versions",
asyncHandler(listEligibleVersionsForReleaseTarget),
);

export const releaseTargetsRouter = Router({ mergeParams: true })
.post("/state", asyncHandler(getReleaseTargetStates))
Expand Down
83 changes: 83 additions & 0 deletions apps/api/src/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/workspaces/{workspaceId}/release-targets/{releaseTargetKey}/eligible-versions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* List versions eligible for a release target
* @description Returns deployment versions that currently pass every policy rule for this release target. An optional CEL filter narrows the result; pagination is applied to the filtered set. Use the "version" variable in the CEL expression to access version properties.
*/
post: operations["listEligibleVersionsForReleaseTarget"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/workspaces/{workspaceId}/release-targets/{releaseTargetKey}/jobs": {
parameters: {
query?: never;
Expand Down Expand Up @@ -4984,6 +5004,69 @@ export interface operations {
};
};
};
listEligibleVersionsForReleaseTarget: {
parameters: {
query?: {
/** @description Maximum number of items to return */
limit?: number;
/** @description Number of items to skip */
offset?: number;
};
header?: never;
path: {
/** @description ID of the workspace */
workspaceId: string;
/** @description Key of the release target */
releaseTargetKey: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description CEL expression to filter eligible versions. Defaults to "true" (all eligible versions). */
filter?: string;
};
};
};
Comment on lines +5024 to +5031
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Request body is modeled as required despite optional filter semantics.

requestBody is required here, which forces clients to send {} even when no CEL filter is provided. That conflicts with the endpoint description and PR intent of optional body-based filtering. Make the request body optional in the OpenAPI source spec (required: false or omitted) and regenerate this file.

As per coding guidelines, "Do not hand-edit generated OpenAPI output (src/types/openapi.ts and openapi/openapi.json). Regenerate using the generate command instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/types/openapi.ts` around lines 5024 - 5031, The OpenAPI output
marks requestBody as required even though the CEL filter is optional; update the
OpenAPI source spec to make the request body optional (set requestBody.required:
false or remove the required flag) so the generated type for requestBody is
optional, then run the project OpenAPI generation command to regenerate
src/types/openapi.ts; focus your change around the requestBody and filter schema
in the endpoint definition before regenerating.

responses: {
/** @description Eligible versions for the release target */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
items: components["schemas"]["DeploymentVersion"][];
/** @description Maximum number of items returned */
limit: number;
/** @description Number of items skipped */
offset: number;
/** @description Total number of items available */
total: number;
};
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Resource not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
getJobsForReleaseTarget: {
parameters: {
query?: {
Expand Down
Loading
Loading