@@ -8,7 +8,7 @@ import path from 'path';
88import GraphqlService from './GraphqlService.mjs' ;
99import { FETCH_ISSUES_FOR_SYNC , DEFAULT_QUERY_LIMITS } from './queries/issueQueries.mjs' ;
1010import { GET_ISSUE_ID , UPDATE_ISSUE } from './queries/mutations.mjs' ;
11- import { FETCH_RELEASES } from './queries/releaseQueries.mjs' ;
11+ import { FETCH_RELEASES , FETCH_LATEST_RELEASE } from './queries/releaseQueries.mjs' ;
1212
1313const issueSyncConfig = aiConfig . issueSync ;
1414
@@ -60,12 +60,13 @@ class SyncService extends Base {
6060 *
6161 * This method orchestrates the entire bi-directional sync workflow in a specific order
6262 * to ensure data integrity and minimize conflicts:
63- * 1. Fetches and caches GitHub release data to aid in archiving.
64- * 2. Loads the persistent metadata from the last sync.
63+ * 1. Loads the persistent metadata from the last sync.
64+ * 2. Fetches and caches GitHub release data, using the loaded metadata to perform a
65+ * quick check and avoid unnecessary fetches.
6566 * 3. **Pushes** any local changes (detected via content hash comparison) to GitHub.
6667 * 4. **Pulls** the latest changes from GitHub, updating local files.
6768 * 5. Syncs release notes into local Markdown files.
68- * 6. Saves the updated metadata to disk for the next run.
69+ * 6. Saves the updated metadata (including the new release cache) to disk for the next run.
6970 *
7071 * @returns {Promise<object> } A comprehensive object containing detailed statistics and timing
7172 * information about all operations performed during the sync, conforming to the
@@ -87,10 +88,11 @@ class SyncService extends Base {
8788 async runFullSync ( ) {
8889 const startTime = new Date ( ) ;
8990
90- await this . #fetchAndCacheReleases( ) ;
91-
9291 const metadata = await this . #loadMetadata( ) ;
9392
93+ // Pass metadata to check cache
94+ await this . #fetchAndCacheReleases( metadata ) ;
95+
9496 // 1. Push local changes
9597 const pushStats = await this . #pushToGitHub( metadata ) ;
9698
@@ -105,7 +107,11 @@ class SyncService extends Base {
105107 newMetadata . pushFailures = newMetadata . pushFailures . filter ( failedId => ! newMetadata . issues [ failedId ] ) ;
106108 }
107109
108- // 5. Save metadata
110+ // 5. Cache releases in metadata for next run
111+ newMetadata . releases = this . releases ;
112+ newMetadata . releasesLastFetched = new Date ( ) . toISOString ( ) ;
113+
114+ // 6. Save metadata
109115 await this . #saveMetadata( newMetadata ) ;
110116
111117 const endTime = new Date ( ) ;
@@ -182,18 +188,55 @@ class SyncService extends Base {
182188 }
183189
184190 /**
185- * Fetches all releases from GitHub using GraphQL with automatic pagination.
191+ * Fetches releases from GitHub using an optimized two-phase approach.
192+ *
193+ * This optimization is necessary because the GitHub GraphQL `releases` endpoint does not
194+ * support a `since` parameter, making simple delta-based fetching impossible.
195+ *
196+ * First, it performs a quick check to see if the latest release is already cached.
197+ * If not, it performs a full, paginated fetch with an early-exit optimization that
198+ * stops querying when it reaches releases older than the `syncStartDate`.
199+ *
200+ * @param {object } metadata The sync metadata containing the cached releases.
186201 * @private
187202 */
188- async #fetchAndCacheReleases( ) {
189- logger . info ( 'Fetching and caching releases via GraphQL...' ) ;
203+ async #fetchAndCacheReleases( metadata ) {
204+ logger . info ( 'Checking for new releases...' ) ;
205+
206+ // Phase 1: Quick check against the cached latest release
207+ if ( metadata . releases && metadata . releases . length > 0 ) {
208+ try {
209+ const latestData = await GraphqlService . query ( FETCH_LATEST_RELEASE , {
210+ owner : aiConfig . owner ,
211+ repo : aiConfig . repo
212+ } ) ;
213+
214+ const latestRelease = latestData . repository . latestRelease ;
215+ const cachedLatest = metadata . releases [ metadata . releases . length - 1 ] ; // Releases are sorted ascending by date
216+
217+ if ( latestRelease && cachedLatest &&
218+ latestRelease . tagName === cachedLatest . tagName &&
219+ latestRelease . publishedAt === cachedLatest . publishedAt ) {
220+ logger . info ( `✅ Releases are up-to-date (latest: ${ latestRelease . tagName } )` ) ;
221+ this . releases = metadata . releases ;
222+ return ;
223+ }
224+
225+ logger . info ( `New release detected: ${ latestRelease . tagName } (cached was: ${ cachedLatest ?. tagName } )` ) ;
226+ } catch ( e ) {
227+ logger . warn ( `Could not check latest release, falling back to full fetch: ${ e . message } ` ) ;
228+ }
229+ }
230+
231+ // Phase 2: Full fetch with early exit
232+ logger . info ( 'Fetching releases from GitHub via GraphQL...' ) ;
190233
191234 let allReleases = [ ] ;
192235 let hasNextPage = true ;
193236 let cursor = null ;
194237 const maxReleases = issueSyncConfig . maxReleases ;
238+ const startDate = new Date ( issueSyncConfig . syncStartDate ) ;
195239
196- // Paginate through all releases
197240 while ( hasNextPage && allReleases . length < maxReleases ) {
198241 const data = await GraphqlService . query ( FETCH_RELEASES , {
199242 owner : aiConfig . owner ,
@@ -203,25 +246,41 @@ class SyncService extends Base {
203246 } ) ;
204247
205248 const releases = data . repository . releases ;
249+
250+ if ( releases . nodes . length === 0 ) {
251+ logger . debug ( 'No more releases found in pagination.' ) ;
252+ break ;
253+ }
254+
255+ // Check if oldest release in this batch is before our cutoff
256+ const oldestInBatch = releases . nodes [ releases . nodes . length - 1 ] ;
257+ const oldestDate = new Date ( oldestInBatch . publishedAt ) ;
258+
259+ // Add all releases from the batch for now; we will filter after the loop
206260 allReleases . push ( ...releases . nodes ) ;
207261
262+ logger . debug ( `Fetched ${ releases . nodes . length } releases (total raw: ${ allReleases . length } )` ) ;
263+
264+ // Early exit if oldest release in batch is before our cutoff
265+ if ( oldestDate < startDate ) {
266+ logger . info ( `Reached releases published before ${ issueSyncConfig . syncStartDate } , stopping pagination.` ) ;
267+ break ;
268+ }
269+
208270 hasNextPage = releases . pageInfo . hasNextPage ;
209271 cursor = releases . pageInfo . endCursor ;
210-
211- logger . debug ( `Fetched ${ releases . nodes . length } releases (total: ${ allReleases . length } )` ) ;
212272 }
213273
214- // Filter by syncStartDate
215- const startDate = new Date ( issueSyncConfig . syncStartDate ) ;
274+ // Now, filter and sort the collected releases
216275 this . releases = allReleases
217276 . filter ( release => new Date ( release . publishedAt ) >= startDate )
218277 . sort ( ( a , b ) => new Date ( a . publishedAt ) - new Date ( b . publishedAt ) ) ;
219278
220279 if ( this . releases . length === 0 ) {
221280 logger . warn ( `⚠️ No releases found since syncStartDate (${ issueSyncConfig . syncStartDate } ). Archiving may fall back to default.` ) ;
281+ } else {
282+ logger . info ( `Found and cached ${ this . releases . length } releases since ${ issueSyncConfig . syncStartDate } .` ) ;
222283 }
223-
224- logger . info ( `Found and cached ${ this . releases . length } releases since ${ issueSyncConfig . syncStartDate } .` ) ;
225284 }
226285
227286 /**
0 commit comments