From 707a401e4aae47e361f7920e77da6fd4f56a4a96 Mon Sep 17 00:00:00 2001 From: "seer-by-sentry[bot]" <157164994+seer-by-sentry[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:52:57 +0000 Subject: [PATCH 1/5] fix(changelog): Add retry and robust error handling for GitHub GraphQL --- src/utils/changelog.ts | 65 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/utils/changelog.ts b/src/utils/changelog.ts index 4ed642c5c..023716c25 100644 --- a/src/utils/changelog.ts +++ b/src/utils/changelog.ts @@ -1943,12 +1943,65 @@ export async function getPRAndLabelsFromCommit(hashes: string[]): Promise< } }`; logger.trace('Running graphql query:', graphqlQuery); - Object.assign( - commitInfo, - ((await getGitHubClient().graphql(graphqlQuery)) as CommitInfoResult) - .repository - ); - logger.trace('Query result:', commitInfo); + + // Retry logic for 5xx errors + const maxRetries = 3; + let lastError: Error | null = null; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const response = await getGitHubClient().graphql(graphqlQuery); + + // Validate response structure before accessing .repository + if (!response || typeof response !== 'object') { + throw new Error('GraphQL response is not a valid object'); + } + + const typedResponse = response as CommitInfoResult; + + // Check if repository property exists + if (!typedResponse.repository) { + throw new Error('GraphQL response missing "repository" property'); + } + + Object.assign(commitInfo, typedResponse.repository); + logger.trace('Query result:', commitInfo); + + // Success - break out of retry loop + lastError = null; + break; + } catch (error: any) { + lastError = error; + + // Check if it's a 5xx error that we should retry + const statusCode = error.status || error.response?.status; + const is5xxError = statusCode >= 500 && statusCode < 600; + + if (is5xxError && attempt < maxRetries - 1) { + const backoffMs = Math.pow(2, attempt) * 1000; // Exponential backoff: 1s, 2s, 4s + logger.warn( + `GraphQL query failed with ${statusCode} (attempt ${attempt + 1}/${maxRetries}). ` + + `Retrying in ${backoffMs}ms...` + ); + await new Promise(resolve => setTimeout(resolve, backoffMs)); + continue; + } + + // Non-5xx error or max retries reached - log and continue with next chunk + logger.warn( + `Failed to fetch PR info for chunk ${chunk + 1}/${chunkCount} ` + + `(${subset.length} commits): ${error.message || error}. ` + + `Skipping this chunk and continuing with remaining commits.` + ); + + // Initialize empty commit info for all hashes in this failed chunk + for (const hash of subset) { + commitInfo[`C${hash}`] = null; + } + + break; // Exit retry loop + } + } } return Object.fromEntries( From 6af5dbd0f7ad16dea9052cdb13ba5087300aedbc Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 9 Jan 2026 22:21:29 +0000 Subject: [PATCH 2/5] fix(changelog): Simplify retry logic and fix missing repository validation Fixes CRAFT-8 Remove redundant manual retry loop since @octokit/plugin-retry already handles 5xx retries automatically (up to 3 times with exponential backoff). The actual issue was missing response validation - GitHub GraphQL can return HTTP 200 with missing/null repository field, causing TypeError when accessing .repository properties. Changes: - Remove 37 lines of manual retry logic (duplicate of plugin behavior) - Keep essential response validation with ?.repository check - Maintain graceful error handling for all failure scenarios This fixes three scenarios: 1. HTTP 502 errors: Handled by @octokit/plugin-retry 2. HTTP 200 with errors array: Caught by try-catch 3. HTTP 200 with missing repository: Caught by validation check --- src/utils/changelog.ts | 73 +++++++++++------------------------------- 1 file changed, 18 insertions(+), 55 deletions(-) diff --git a/src/utils/changelog.ts b/src/utils/changelog.ts index 023716c25..330ccd9e8 100644 --- a/src/utils/changelog.ts +++ b/src/utils/changelog.ts @@ -1943,63 +1943,26 @@ export async function getPRAndLabelsFromCommit(hashes: string[]): Promise< } }`; logger.trace('Running graphql query:', graphqlQuery); - - // Retry logic for 5xx errors - const maxRetries = 3; - let lastError: Error | null = null; - - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - const response = await getGitHubClient().graphql(graphqlQuery); - - // Validate response structure before accessing .repository - if (!response || typeof response !== 'object') { - throw new Error('GraphQL response is not a valid object'); - } - - const typedResponse = response as CommitInfoResult; - - // Check if repository property exists - if (!typedResponse.repository) { - throw new Error('GraphQL response missing "repository" property'); - } - - Object.assign(commitInfo, typedResponse.repository); - logger.trace('Query result:', commitInfo); - - // Success - break out of retry loop - lastError = null; - break; - } catch (error: any) { - lastError = error; - - // Check if it's a 5xx error that we should retry - const statusCode = error.status || error.response?.status; - const is5xxError = statusCode >= 500 && statusCode < 600; - - if (is5xxError && attempt < maxRetries - 1) { - const backoffMs = Math.pow(2, attempt) * 1000; // Exponential backoff: 1s, 2s, 4s - logger.warn( - `GraphQL query failed with ${statusCode} (attempt ${attempt + 1}/${maxRetries}). ` + - `Retrying in ${backoffMs}ms...` - ); - await new Promise(resolve => setTimeout(resolve, backoffMs)); - continue; - } - - // Non-5xx error or max retries reached - log and continue with next chunk - logger.warn( - `Failed to fetch PR info for chunk ${chunk + 1}/${chunkCount} ` + + + try { + const response = await getGitHubClient().graphql(graphqlQuery); + const typedResponse = response as CommitInfoResult; + + if (!typedResponse?.repository) { + throw new Error('GraphQL response missing "repository" property'); + } + + Object.assign(commitInfo, typedResponse.repository); + logger.trace('Query result:', commitInfo); + } catch (error: any) { + logger.warn( + `Failed to fetch PR info for chunk ${chunk + 1}/${chunkCount} ` + `(${subset.length} commits): ${error.message || error}. ` + `Skipping this chunk and continuing with remaining commits.` - ); - - // Initialize empty commit info for all hashes in this failed chunk - for (const hash of subset) { - commitInfo[`C${hash}`] = null; - } - - break; // Exit retry loop + ); + + for (const hash of subset) { + commitInfo[`C${hash}`] = null; } } } From 2118da6f5e5d04c0d4d29bcd37852f6f82304efd Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 9 Jan 2026 22:23:38 +0000 Subject: [PATCH 3/5] feat(changelog): Add Sentry error tracking for GraphQL failures Capture GraphQL changelog failures to Sentry with rich context for monitoring: - Tag 'error_type' for easy filtering (graphql_changelog_failure) - Tags for chunk_number and total_chunks for tracking failure patterns - Context with chunk details, commit count, HTTP status, and error name This enables: - Tracking frequency of GraphQL failures in production - Identifying patterns (e.g., specific chunks that fail consistently) - Monitoring the effectiveness of the retry plugin - Better visibility into GitHub API reliability issues The error is still logged as a warning and handled gracefully (skipping the failed chunk and continuing with remaining commits). --- src/utils/changelog.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/changelog.ts b/src/utils/changelog.ts index 330ccd9e8..04027678c 100644 --- a/src/utils/changelog.ts +++ b/src/utils/changelog.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { load } from 'js-yaml'; import { marked, type Token, type Tokens } from 'marked'; +import { captureException, withScope } from '@sentry/node'; import { logger } from '../logger'; import { @@ -1955,6 +1956,21 @@ export async function getPRAndLabelsFromCommit(hashes: string[]): Promise< Object.assign(commitInfo, typedResponse.repository); logger.trace('Query result:', commitInfo); } catch (error: any) { + // Capture to Sentry with context for tracking GraphQL failures + withScope(scope => { + scope.setTag('error_type', 'graphql_changelog_failure'); + scope.setTag('chunk_number', `${chunk + 1}`); + scope.setTag('total_chunks', `${chunkCount}`); + scope.setContext('graphql_chunk', { + chunkIndex: chunk + 1, + totalChunks: chunkCount, + commitsInChunk: subset.length, + httpStatus: error.status || error.response?.status, + errorName: error.name, + }); + captureException(error); + }); + logger.warn( `Failed to fetch PR info for chunk ${chunk + 1}/${chunkCount} ` + `(${subset.length} commits): ${error.message || error}. ` + From 1d0045759cddeef28139f046782f87dbbc2faace Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 9 Jan 2026 22:24:25 +0000 Subject: [PATCH 4/5] feat(changelog): Include GraphQL query details in Sentry context Add the full GraphQL query and additional metrics to Sentry error context: - GraphQL query string (for reproducing issues) - Query length (to identify if query size correlates with failures) - Total hashes being processed (context for chunk failures) This enables debugging questions like: - Do larger queries fail more often? - Are specific query patterns causing issues? - Can we reproduce failures with the exact query? --- src/utils/changelog.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/changelog.ts b/src/utils/changelog.ts index 04027678c..9a1d69ff3 100644 --- a/src/utils/changelog.ts +++ b/src/utils/changelog.ts @@ -1965,9 +1965,14 @@ export async function getPRAndLabelsFromCommit(hashes: string[]): Promise< chunkIndex: chunk + 1, totalChunks: chunkCount, commitsInChunk: subset.length, + totalHashes: hashes.length, httpStatus: error.status || error.response?.status, errorName: error.name, }); + scope.setContext('graphql_query', { + query: graphqlQuery, + queryLength: graphqlQuery.length, + }); captureException(error); }); From 09729f17e46d44acea6e1da6d692ceef63dc8dff Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 9 Jan 2026 22:26:10 +0000 Subject: [PATCH 5/5] refactor(changelog): Consolidate Sentry context and remove queryLength Move GraphQL query into the main graphql_chunk context and remove the redundant queryLength metric. The query string itself is more useful for debugging, and totalHashes already provides the scale information needed. --- src/utils/changelog.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utils/changelog.ts b/src/utils/changelog.ts index 9a1d69ff3..96580158f 100644 --- a/src/utils/changelog.ts +++ b/src/utils/changelog.ts @@ -1968,10 +1968,7 @@ export async function getPRAndLabelsFromCommit(hashes: string[]): Promise< totalHashes: hashes.length, httpStatus: error.status || error.response?.status, errorName: error.name, - }); - scope.setContext('graphql_query', { query: graphqlQuery, - queryLength: graphqlQuery.length, }); captureException(error); });