-
Notifications
You must be signed in to change notification settings - Fork 278
fix: defensive guards for project_id coercion, labels query, and null MR response #438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1281,6 +1281,22 @@ async function handleGitLabError(response: import("node-fetch").Response): Promi | |||||||||
| * @throws {Error} If GITLAB_ALLOWED_PROJECT_IDS is set and the requested project is not in the whitelist | ||||||||||
| */ | ||||||||||
| function getEffectiveProjectId(projectId: string): string { | ||||||||||
| // Guard against z.coerce.string() converting undefined/null to literal strings | ||||||||||
| if (!projectId || projectId === "undefined" || projectId === "null") { | ||||||||||
| if (GITLAB_ALLOWED_PROJECT_IDS.length === 1) { | ||||||||||
| return GITLAB_ALLOWED_PROJECT_IDS[0]; | ||||||||||
| } | ||||||||||
| if (GITLAB_ALLOWED_PROJECT_IDS.length > 1) { | ||||||||||
| throw new Error( | ||||||||||
| `Multiple projects allowed (${GITLAB_ALLOWED_PROJECT_IDS.join(", ")}). Please specify a project ID.` | ||||||||||
| ); | ||||||||||
| } | ||||||||||
| if (GITLAB_PROJECT_ID) { | ||||||||||
| return GITLAB_PROJECT_ID; | ||||||||||
| } | ||||||||||
| throw new Error("No project ID provided and GITLAB_PROJECT_ID is not set"); | ||||||||||
|
Comment on lines
+1284
to
+1297
|
||||||||||
| } | ||||||||||
|
|
||||||||||
| if (GITLAB_ALLOWED_PROJECT_IDS.length > 0) { | ||||||||||
| // If there's only one allowed project, use it as default | ||||||||||
| if (GITLAB_ALLOWED_PROJECT_IDS.length === 1 && !projectId) { | ||||||||||
|
|
@@ -1509,16 +1525,10 @@ async function listIssues( | |||||||||
| // Add all query parameters | ||||||||||
| Object.entries(options).forEach(([key, value]) => { | ||||||||||
| if (value !== undefined) { | ||||||||||
| const keys = ["labels", "assignee_username"]; | ||||||||||
| if (keys.includes(key)) { | ||||||||||
| if (Array.isArray(value)) { | ||||||||||
| // Handle array of labels | ||||||||||
| value.forEach(label => { | ||||||||||
| url.searchParams.append(`${key}[]`, label.toString()); | ||||||||||
| }); | ||||||||||
| } else if (value) { | ||||||||||
| url.searchParams.append(`${key}[]`, value.toString()); | ||||||||||
| } | ||||||||||
| if (key === "labels" && Array.isArray(value)) { | ||||||||||
| url.searchParams.append(key, value.join(",")); | ||||||||||
|
||||||||||
| url.searchParams.append(key, value.join(",")); | |
| if (value.length > 0) { | |
| url.searchParams.append(key, value.join(",")); | |
| } |
Copilot
AI
Apr 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The query serialization for labels changed semantics (comma-separated string, and potential labels= for empty arrays). Adding a focused test that asserts the outgoing request URL contains labels=bug,feature (and omits labels for []) would lock in the intended GitLab API behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not actually handle the null response case described in the PR. listMergeRequests() still parses the response with z.array(GitLabMergeRequestSchema).parse(data) before returning, so if GitLab returns null, it will throw inside listMergeRequests() and this mergeRequests ?? [] fallback is never reached.
Could we move the fallback into listMergeRequests() itself, for example by normalizing data ?? [] before the zod array parse, or by changing the parser to explicitly accept null and return [] there? That keeps the behavior consistent for every caller instead of only this tool-call branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested shape:
const data = await response.json();
const normalizedData = data ?? [];
return z.array(GitLabMergeRequestSchema).parse(normalizedData);If we only want to tolerate null and still reject other unexpected shapes, this keeps the existing zod validation intact while making the documented null -> [] behavior actually reachable for every listMergeRequests() caller.
Copilot
AI
Apr 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
listMergeRequests() is typed to return GitLabMergeRequest[] and currently z.array(...).parse(data) will throw if the API returns null. Coalescing here to [] is likely redundant and can hide unexpected return shapes; consider handling a null/invalid response inside listMergeRequests (or adjusting the zod schema) so the behavior is consistent for all callers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Special-casing the literal strings "undefined" and "null" means a real project whose ID/path is exactly "undefined" or "null" can no longer be targeted. If the intent is only to handle
z.coerce.string()/ null inputs, it’s safer to fix this at the schema boundary (e.g., preprocess null/"null" to undefined) or carry anundefined | stringtype into this function rather than reserving specific string values.