1+ import { Octokit } from "@octokit/rest" ;
2+ import fs from 'fs' ;
3+ import path from 'path' ;
4+
5+ const octokit = new Octokit ( {
6+ auth : process . env . GITHUB_TOKEN ,
7+ } ) ;
8+
9+ async function getLatestCommit ( owner , repo , filePath = '' ) {
10+ try {
11+ const response = await octokit . rest . repos . listCommits ( {
12+ owner,
13+ repo,
14+ path : filePath ,
15+ per_page : 1
16+ } ) ;
17+ return response . data [ 0 ] ;
18+ } catch ( error ) {
19+ console . error ( `❌ Error fetching commits for ${ owner } /${ repo } : ${ error . message } ` ) ;
20+ return null ;
21+ }
22+ }
23+
24+ async function getFileChanges ( owner , repo , oldCommit , newCommit , filePaths ) {
25+ try {
26+ const comparison = await octokit . rest . repos . compareCommits ( {
27+ owner,
28+ repo,
29+ base : oldCommit ,
30+ head : newCommit
31+ } ) ;
32+
33+ const relevantFiles = comparison . data . files . filter ( file =>
34+ filePaths . some ( trackPath => {
35+ const normalizedTrackPath = trackPath . startsWith ( '/' ) ? trackPath . slice ( 1 ) : trackPath ;
36+ return file . filename . includes ( normalizedTrackPath ) ||
37+ normalizedTrackPath . includes ( file . filename ) ;
38+ } )
39+ ) ;
40+
41+ return {
42+ totalChanges : comparison . data . files . length ,
43+ relevantChanges : relevantFiles . length ,
44+ files : relevantFiles . map ( file => ( {
45+ filename : file . filename ,
46+ status : file . status ,
47+ additions : file . additions ,
48+ deletions : file . deletions ,
49+ patch : file . patch
50+ } ) )
51+ } ;
52+ } catch ( error ) {
53+ console . error ( `❌ Error comparing commits: ${ error . message } ` ) ;
54+ return null ;
55+ }
56+ }
57+
58+ function findMetadataFiles ( dir ) {
59+ const metadataFiles = [ ] ;
60+
61+ function searchRecursively ( currentDir ) {
62+ const entries = fs . readdirSync ( currentDir , { withFileTypes : true } ) ;
63+
64+ for ( const entry of entries ) {
65+ const fullPath = path . join ( currentDir , entry . name ) ;
66+
67+ if ( entry . isDirectory ( ) ) {
68+ searchRecursively ( fullPath ) ;
69+ } else if ( entry . name === 'metadata.json' ) {
70+ metadataFiles . push ( fullPath ) ;
71+ }
72+ }
73+ }
74+
75+ searchRecursively ( dir ) ;
76+ return metadataFiles ;
77+ }
78+
79+ async function main ( ) {
80+ console . log ( '🔍 Starting metadata update check...\n' ) ;
81+
82+ const metadataFiles = findMetadataFiles ( './repositories' ) ;
83+ console . log ( `📁 Found ${ metadataFiles . length } metadata files\n` ) ;
84+
85+ let totalChecked = 0 ;
86+ let updatesAvailable = 0 ;
87+ let errors = 0 ;
88+
89+ const detailedOutput = process . env . DETAILED_OUTPUT === 'true' ;
90+ const onlyShowUpdates = process . env . ONLY_SHOW_UPDATES === 'true' ;
91+
92+ for ( const metadataFile of metadataFiles ) {
93+ try {
94+ totalChecked ++ ;
95+ const metadata = JSON . parse ( fs . readFileSync ( metadataFile , 'utf8' ) ) ;
96+
97+ // Skip showing repository info if only showing updates and this repo is up to date
98+ const shouldShowBasicInfo = ! onlyShowUpdates ;
99+
100+ if ( shouldShowBasicInfo ) {
101+ console . log ( `📦 Checking: ${ metadata . name } (${ metadata . owner } /${ metadata . repo } )` ) ;
102+ console . log ( ` Repository: https://github.com/${ metadata . owner } /${ metadata . repo } ` ) ;
103+ console . log ( ` Current commit: ${ metadata . commit } ` ) ;
104+ console . log ( ` Path: ${ metadata . path || '/' } ` ) ;
105+ }
106+
107+ // Get latest commit from the repository
108+ const latestCommit = await getLatestCommit (
109+ metadata . owner ,
110+ metadata . repo ,
111+ metadata . path
112+ ) ;
113+
114+ if ( ! latestCommit ) {
115+ if ( shouldShowBasicInfo ) {
116+ console . log ( ` ❌ Could not fetch latest commit\n` ) ;
117+ }
118+ errors ++ ;
119+ continue ;
120+ }
121+
122+ if ( shouldShowBasicInfo ) {
123+ console . log ( ` Latest commit: ${ latestCommit . sha } ` ) ;
124+ console . log ( ` Latest commit date: ${ latestCommit . commit . committer . date } ` ) ;
125+ console . log ( ` Latest commit message: "${ latestCommit . commit . message . split ( '\n' ) [ 0 ] } "` ) ;
126+ }
127+
128+ if ( metadata . commit === latestCommit . sha ) {
129+ if ( ! onlyShowUpdates ) {
130+ console . log ( ` ✅ Up to date\n` ) ;
131+ }
132+ } else {
133+ // Get file paths to track
134+ let filePaths = [ ] ;
135+ if ( Array . isArray ( metadata . files ) ) {
136+ filePaths = metadata . files . map ( file => {
137+ if ( typeof file === 'string' ) {
138+ return path . join ( metadata . path || '/' , file ) . replace ( / \\ / g, '/' ) ;
139+ } else if ( file . source ) {
140+ return path . join ( metadata . path || '/' , file . source ) . replace ( / \\ / g, '/' ) ;
141+ }
142+ return '' ;
143+ } ) . filter ( p => p ) ;
144+ }
145+
146+ // Check if tracked files have actually changed
147+ let hasRelevantChanges = false ;
148+ let changes = null ;
149+
150+ if ( filePaths . length > 0 ) {
151+ changes = await getFileChanges (
152+ metadata . owner ,
153+ metadata . repo ,
154+ metadata . commit ,
155+ latestCommit . sha ,
156+ filePaths
157+ ) ;
158+
159+ if ( changes && changes . relevantChanges > 0 ) {
160+ hasRelevantChanges = true ;
161+ }
162+ } else {
163+ // If no specific files to track, assume any commit means changes
164+ hasRelevantChanges = true ;
165+ }
166+
167+ if ( ! hasRelevantChanges ) {
168+ if ( ! onlyShowUpdates ) {
169+ console . log ( ` ✅ Up to date (no relevant file changes)\n` ) ;
170+ }
171+ } else {
172+ // Always show update available info, regardless of onlyShowUpdates setting
173+ if ( onlyShowUpdates ) {
174+ console . log ( `📦 Checking: ${ metadata . name } (${ metadata . owner } /${ metadata . repo } )` ) ;
175+ console . log ( ` Repository: https://github.com/${ metadata . owner } /${ metadata . repo } ` ) ;
176+ console . log ( ` Current commit: ${ metadata . commit } ` ) ;
177+ console . log ( ` Latest commit: ${ latestCommit . sha } ` ) ;
178+ console . log ( ` Latest commit date: ${ latestCommit . commit . committer . date } ` ) ;
179+ console . log ( ` Latest commit message: "${ latestCommit . commit . message . split ( '\n' ) [ 0 ] } "` ) ;
180+ }
181+ console . log ( ` 🔄 UPDATE AVAILABLE!` ) ;
182+ console . log ( ` 📄 Compare commits: https://github.com/${ metadata . owner } /${ metadata . repo } /compare/${ metadata . commit } ...${ latestCommit . sha } ` ) ;
183+ updatesAvailable ++ ;
184+
185+ // Get detailed changes if requested
186+ if ( detailedOutput && changes ) {
187+ console . log ( ` 📊 Total repository changes: ${ changes . totalChanges } files` ) ;
188+ console . log ( ` 📊 Relevant file changes: ${ changes . relevantChanges } files` ) ;
189+
190+ if ( changes . files . length > 0 ) {
191+ console . log ( ` 📄 Changed files:` ) ;
192+ for ( const file of changes . files ) {
193+ console . log ( ` - ${ file . filename } (${ file . status } ) [+${ file . additions } /-${ file . deletions } ]` ) ;
194+ }
195+ }
196+ }
197+ console . log ( '' ) ;
198+ }
199+ }
200+
201+ // Small delay to avoid rate limiting
202+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
203+
204+ } catch ( error ) {
205+ console . error ( `❌ Error processing ${ metadataFile } : ${ error . message } \n` ) ;
206+ errors ++ ;
207+ }
208+ }
209+
210+ console . log ( '📊 SUMMARY:' ) ;
211+ console . log ( ` Total metadata files checked: ${ totalChecked } ` ) ;
212+ console . log ( ` Updates available: ${ updatesAvailable } ` ) ;
213+ console . log ( ` Errors: ${ errors } ` ) ;
214+ console . log ( ` Up to date: ${ totalChecked - updatesAvailable - errors } ` ) ;
215+
216+ if ( updatesAvailable > 0 ) {
217+ console . log ( `\n🎯 ${ updatesAvailable } repositories have updates available!` ) ;
218+ process . exit ( 0 ) ; // Don't fail the workflow, just inform
219+ } else {
220+ console . log ( '\n✅ All repositories are up to date!' ) ;
221+ }
222+ }
223+
224+ main ( ) . catch ( console . error ) ;
0 commit comments