Skip to content

Commit aa9bc54

Browse files
neo-opus-adatobiu
andauthored
fix(github-workflow): PR + Discussion delta-fetch cutoffs (#12190) (#12193)
PullRequestSyncer.syncPullRequests and DiscussionSyncer.syncDiscussions scanned the entire corpus every run — the dominant driver of the rising GitHub rate-limit hits (issues were already delta-gated; PRs and Discussions were not). Neither GraphQL connection supports a server-side since filter (only issues does), but both already order UPDATED_AT DESC, so this adds a client-side cutoff: stop paginating once a batch oldest item predates the cached high-water mark (max updatedAt across the per-type metadata). A clean-slate run (lastSync null) or an empty cache still traverses the full history. Also removed a misleading since line from the FETCH_PULL_REQUESTS_FOR_SYNC JSDoc. Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
1 parent bb95a80 commit aa9bc54

5 files changed

Lines changed: 85 additions & 1 deletion

File tree

ai/services/github-workflow/queries/pullRequestQueries.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ export const FETCH_PULL_REQUESTS = `
7474
* - $limit: Int!
7575
* - $cursor: String
7676
* - $states: [PullRequestState!]
77-
* - $since: DateTime
7877
* - $maxComments: Int!
7978
* - $maxReviews: Int!
8079
*/

ai/services/github-workflow/sync/DiscussionSyncer.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ class DiscussionSyncer extends Base {
199199
let hasNextPage = true;
200200
let cursor = null;
201201

202+
// Delta cutoff. Mirror the PR/issue delta: the query orders UPDATED_AT DESC and we stop
203+
// paginating once a batch's oldest discussion predates the cached high-water mark. A
204+
// clean-slate run (lastSync === null) or an empty cache traverses the full history.
205+
const cachedDiscDates = Object.values(metadata.discussions || {})
206+
.map(d => Date.parse(d.updatedAt)).filter(t => !isNaN(t));
207+
const sinceCutoff = (metadata.lastSync == null || cachedDiscDates.length === 0)
208+
? 0
209+
: cachedDiscDates.reduce((max, t) => t > max ? t : max, 0);
210+
202211
// Ensure directory exists
203212
await fs.mkdir(issueSyncConfig.discussionsDir, { recursive: true });
204213

@@ -220,6 +229,11 @@ class DiscussionSyncer extends Base {
220229

221230
allDiscussions.push(...discussions.nodes);
222231

232+
// Stop once the oldest discussion in this UPDATED_AT-DESC batch predates the cutoff —
233+
// everything beyond is already current (re-processed in-window items no-op via content-hash).
234+
const oldestDisc = discussions.nodes[discussions.nodes.length - 1];
235+
if (Date.parse(oldestDisc.updatedAt) < sinceCutoff) break;
236+
223237
hasNextPage = discussions.pageInfo.hasNextPage;
224238
cursor = discussions.pageInfo.endCursor;
225239
}

ai/services/github-workflow/sync/PullRequestSyncer.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ class PullRequestSyncer extends Base {
201201
let hasNextPage = true;
202202
let cursor = null;
203203

204+
// Delta cutoff. The `pullRequests` connection (unlike `issues`) has no server-side `since`
205+
// filter, so the query orders UPDATED_AT DESC and we stop paginating once a batch's oldest PR
206+
// predates the cached high-water mark. A clean-slate run (lastSync === null) or an empty cache
207+
// traverses the full history. Mirrors the issue delta semantics — the dominant rate-limit win.
208+
const cachedPrDates = Object.values(metadata.pulls || {})
209+
.map(p => Date.parse(p.updatedAt)).filter(t => !isNaN(t));
210+
const sinceCutoff = (metadata.lastSync == null || cachedPrDates.length === 0)
211+
? 0
212+
: cachedPrDates.reduce((max, t) => t > max ? t : max, 0);
213+
204214
// Ensure directory exists
205215
await fs.mkdir(issueSyncConfig.pullsDir, { recursive: true });
206216

@@ -223,6 +233,11 @@ class PullRequestSyncer extends Base {
223233

224234
allPullRequests.push(...pullRequests.nodes);
225235

236+
// Stop once the oldest PR in this UPDATED_AT-DESC batch predates the cutoff — everything
237+
// beyond is already current (re-processed in-window items no-op via the content-hash skip).
238+
const oldestPr = pullRequests.nodes[pullRequests.nodes.length - 1];
239+
if (Date.parse(oldestPr.updatedAt) < sinceCutoff) break;
240+
226241
hasNextPage = pullRequests.pageInfo.hasNextPage;
227242
cursor = pullRequests.pageInfo.endCursor;
228243
}

test/playwright/unit/ai/services/github-workflow/DiscussionSyncer.spec.mjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,33 @@ test.describe('Neo.ai.services.github-workflow.sync.DiscussionSyncer', () => {
136136
expect(content).toMatch(/^closed: true$/m);
137137
expect(content).toMatch(/^closedAt: '2026-05-01T00:00:00Z'$/m);
138138
});
139+
140+
test('delta cutoff stops discussion pagination once a batch predates the cached high-water mark (#12190)', async () => {
141+
// Mirror of the PR/issue delta: order UPDATED_AT DESC + stop at the cached high-water mark.
142+
const metadata = {
143+
lastSync: '2026-05-01T00:00:00Z',
144+
discussions: {
145+
9001: {updatedAt: '2026-05-01T00:00:00Z', path: 'resources/content/discussions/chunk-1/discussion-9001.md'}
146+
}
147+
};
148+
149+
const discNew = buildDiscussion(8001); discNew.updatedAt = '2026-05-03T00:00:00Z'; // after hwm → fetched
150+
const discOld = buildDiscussion(7001); discOld.updatedAt = '2026-04-29T00:00:00Z'; // before hwm → trips cutoff
151+
const discTooOld = buildDiscussion(6001); discTooOld.updatedAt = '2026-04-28T00:00:00Z'; // must never be fetched
152+
153+
let queryCalls = 0;
154+
GraphqlService.query = async () => {
155+
queryCalls++;
156+
if (queryCalls === 1) return {repository: {discussions: {nodes: [discNew], pageInfo: {hasNextPage: true, endCursor: 'c1'}}}};
157+
if (queryCalls === 2) return {repository: {discussions: {nodes: [discOld], pageInfo: {hasNextPage: true, endCursor: 'c2'}}}};
158+
return {repository: {discussions: {nodes: [discTooOld], pageInfo: {hasNextPage: false, endCursor: null}}}};
159+
};
160+
161+
await DiscussionSyncer.syncDiscussions(metadata);
162+
163+
// Stopped at page 2 (its oldest discussion predates the high-water mark); page 3 never requested.
164+
expect(queryCalls).toBe(2);
165+
});
139166
});
140167

141168
function buildDiscussion(number, config = {}) {

test/playwright/unit/ai/services/github-workflow/PullRequestSyncer.spec.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,35 @@ test.describe('Neo.ai.services.github-workflow.sync.PullRequestSyncer', () => {
149149
await expect(fs.pathExists(releasePath)).resolves.toBe(true);
150150
await expect(fs.pathExists(garbagePath)).resolves.toBe(false);
151151
});
152+
153+
test('delta cutoff stops PR pagination once a batch predates the cached high-water mark (#12190)', async () => {
154+
// The `pullRequests` connection has no server-side `since`, so the syncer orders UPDATED_AT
155+
// DESC and stops paginating at the cached high-water mark. Pre-fix it scanned the full corpus.
156+
const metadata = {
157+
lastSync: '2026-05-01T00:00:00Z',
158+
pulls: {
159+
9001: {state: 'MERGED', updatedAt: '2026-05-01T00:00:00Z', path: 'resources/content/archive/pulls/v12.0.0/chunk-1/pr-9001.md'}
160+
}
161+
};
162+
163+
const prNew = buildPullRequest(8001); prNew.updatedAt = '2026-05-03T00:00:00Z'; // after hwm → fetched
164+
const prOld = buildPullRequest(7001); prOld.updatedAt = '2026-04-29T00:00:00Z'; // before hwm → trips cutoff
165+
const prTooOld = buildPullRequest(6001); prTooOld.updatedAt = '2026-04-28T00:00:00Z'; // must never be fetched
166+
167+
let queryCalls = 0;
168+
GraphqlService.query = async () => {
169+
queryCalls++;
170+
if (queryCalls === 1) return {repository: {pullRequests: {nodes: [prNew], pageInfo: {hasNextPage: true, endCursor: 'c1'}}}};
171+
if (queryCalls === 2) return {repository: {pullRequests: {nodes: [prOld], pageInfo: {hasNextPage: true, endCursor: 'c2'}}}};
172+
return {repository: {pullRequests: {nodes: [prTooOld], pageInfo: {hasNextPage: false, endCursor: null}}}};
173+
};
174+
175+
await PullRequestSyncer.syncPullRequests(metadata);
176+
177+
// Stopped at page 2 (its oldest PR predates the high-water mark); page 3 never requested.
178+
// Pre-fix (full-corpus scan) this would be 3.
179+
expect(queryCalls).toBe(2);
180+
});
152181
});
153182

154183
function buildPullRequest(number) {

0 commit comments

Comments
 (0)