@@ -6,13 +6,30 @@ import { EditorVersionInfo } from '../../model/editorVersionInfo';
66import { RepoVersionInfo } from '../../model/repoVersionInfo' ;
77
88const DOCKERHUB_API = 'https://hub.docker.com/v2/repositories' ;
9- const MAX_IMAGES_PER_CYCLE = 20 ;
10- const RECENT_VERSIONS_TO_CHECK = 5 ;
9+ const TAG_PAGE_SIZE = 100 ;
10+ const MAX_TAG_PAGES = 50 ;
11+ const MAX_RETRIES_PER_CYCLE = 30 ;
12+ const UBUNTU_PLATFORMS = [
13+ 'base' ,
14+ 'linux-il2cpp' ,
15+ 'windows-mono' ,
16+ 'mac-mono' ,
17+ 'ios' ,
18+ 'android' ,
19+ 'webgl' ,
20+ ] as const ;
21+ const WINDOWS_PLATFORMS = [
22+ 'base' ,
23+ 'windows-il2cpp' ,
24+ 'universal-windows-platform' ,
25+ 'appletv' ,
26+ 'android' ,
27+ ] as const ;
1128
1229interface DockerImage {
1330 repository : string ;
1431 tag : string ;
15- baseOs : string ;
32+ baseOs : 'ubuntu' | 'windows' ;
1633 imageType : 'base' | 'hub' | 'editor' ;
1734 targetPlatform ?: string ;
1835 editorVersion ?: string ;
@@ -30,8 +47,8 @@ export class DockerImageReconciler {
3047 private repoVersionFull : string ;
3148 private repoVersionMinor : string ;
3249 private repoVersionMajor : string ;
33- private imagesChecked = 0 ;
3450 private missingImages : MissingImage [ ] = [ ] ;
51+ private retriesDispatched = 0 ;
3552
3653 constructor ( gitHubClient : Octokit , repoVersionInfo : RepoVersionInfo ) {
3754 this . gitHubClient = gitHubClient ;
@@ -41,120 +58,137 @@ export class DockerImageReconciler {
4158 this . repoVersionMajor = String ( major ) ;
4259 }
4360
44- private async isDockerImageMissing ( repository : string , tag : string ) : Promise < boolean > {
45- try {
46- const response = await fetch ( `${ DOCKERHUB_API } /${ repository } /tags/${ tag } ` , {
47- headers : { 'User-Agent' : 'game-ci-versioning-backend/1.0' } ,
48- } ) ;
49- if ( response . status === 404 ) return true ;
50- if ( ! response . ok ) throw new Error ( `HTTP ${ response . status } ` ) ;
51- return false ;
52- } catch ( error ) {
53- logger . warn ( `DockerHub API error checking ${ repository } :${ tag } ` , error ) ;
54- return false ;
61+ async reconcileEditorImages ( versions : EditorVersionInfo [ ] ) : Promise < void > {
62+ if ( versions . length === 0 ) {
63+ return ;
5564 }
56- }
5765
58- async reconcileEditorImages ( versions : EditorVersionInfo [ ] ) : Promise < void > {
59- if ( versions . length === 0 ) return ;
60- const versionsToCheck = versions . slice ( 0 , RECENT_VERSIONS_TO_CHECK ) ;
61- for ( const version of versionsToCheck ) {
62- if ( this . imagesChecked >= MAX_IMAGES_PER_CYCLE ) {
66+ const [ baseTags , hubTags , editorTags ] = await Promise . all ( [
67+ this . fetchExistingTags ( 'unityci/base' ) ,
68+ this . fetchExistingTags ( 'unityci/hub' ) ,
69+ this . fetchExistingTags ( 'unityci/editor' ) ,
70+ ] ) ;
71+
72+ const expectedImages = this . computeExpectedImages ( versions ) ;
73+ const tagSetByRepo : Record < string , Set < string > > = {
74+ 'unityci/base' : baseTags ,
75+ 'unityci/hub' : hubTags ,
76+ 'unityci/editor' : editorTags ,
77+ } ;
78+
79+ for ( const image of expectedImages ) {
80+ if ( this . retriesDispatched >= MAX_RETRIES_PER_CYCLE ) {
6381 break ;
6482 }
65- await this . checkVersionImages ( version ) ;
83+ const existing = tagSetByRepo [ image . repository ] ;
84+ if ( existing . has ( image . tag ) ) {
85+ continue ;
86+ }
87+ const dispatchedRetry = await this . dispatchRetry ( image ) ;
88+ if ( dispatchedRetry ) {
89+ this . retriesDispatched += 1 ;
90+ }
91+ this . missingImages . push ( { image, dispatchedRetry } ) ;
6692 }
67- await this . reportResults ( ) ;
68- }
69-
70- private async checkVersionImages ( version : EditorVersionInfo ) : Promise < void > {
71- const { version : editorVersion , changeSet : changeset } = version ;
7293
73- const baseImages = [
74- { repo : 'base' as const , tag : `ubuntu-${ this . repoVersionFull } ` , os : 'ubuntu' as const } ,
75- { repo : 'base' as const , tag : `windows-${ this . repoVersionFull } ` , os : 'windows' as const } ,
76- { repo : 'hub' as const , tag : `ubuntu-${ this . repoVersionFull } ` , os : 'ubuntu' as const } ,
77- { repo : 'hub' as const , tag : `windows-${ this . repoVersionFull } ` , os : 'windows' as const } ,
78- ] ;
94+ await this . reportResults ( expectedImages . length ) ;
95+ }
7996
80- for ( const { repo, tag, os } of baseImages ) {
81- const image : DockerImage = {
82- repository : `unityci/${ repo } ` ,
83- tag,
84- baseOs : os ,
85- imageType : repo ,
86- } ;
87- await this . checkImage ( image ) ;
97+ private async fetchExistingTags ( repository : string ) : Promise < Set < string > > {
98+ const tags = new Set < string > ( ) ;
99+ let nextUrl : string | null =
100+ `${ DOCKERHUB_API } /${ repository } /tags?page_size=${ TAG_PAGE_SIZE } &name=${ this . repoVersionFull } ` ;
101+ let pagesFetched = 0 ;
102+
103+ while ( nextUrl && pagesFetched < MAX_TAG_PAGES ) {
104+ try {
105+ const response = await fetch ( nextUrl , {
106+ headers : { 'User-Agent' : 'game-ci-versioning-backend/1.0' } ,
107+ } ) ;
108+ if ( ! response . ok ) {
109+ logger . warn ( `DockerHub list tags failed for ${ repository } : HTTP ${ response . status } ` ) ;
110+ break ;
111+ }
112+ const body = ( await response . json ( ) ) as {
113+ results ?: { name : string } [ ] ;
114+ next ?: string | null ;
115+ } ;
116+ for ( const result of body . results ?? [ ] ) {
117+ tags . add ( result . name ) ;
118+ }
119+ nextUrl = body . next ?? null ;
120+ pagesFetched += 1 ;
121+ } catch ( error ) {
122+ logger . warn ( `DockerHub list tags error for ${ repository } ` , error ) ;
123+ break ;
124+ }
88125 }
89126
90- const ubuntuPlatforms = [
91- 'base' ,
92- 'linux-il2cpp' ,
93- 'windows-mono' ,
94- 'mac-mono' ,
95- 'ios' ,
96- 'android' ,
97- 'webgl' ,
98- ] as const ;
99- for ( const platform of ubuntuPlatforms ) {
100- if ( this . imagesChecked >= MAX_IMAGES_PER_CYCLE ) break ;
101- await this . checkImage ( {
102- repository : 'unityci/editor' ,
103- tag : `ubuntu-${ editorVersion } -${ platform } -${ this . repoVersionFull } ` ,
104- baseOs : 'ubuntu' ,
105- imageType : 'editor' ,
106- targetPlatform : platform ,
107- editorVersion,
108- changeset,
109- } ) ;
110- }
127+ return tags ;
128+ }
111129
112- const windowsPlatforms = [
113- 'base' ,
114- 'windows-il2cpp' ,
115- 'universal-windows-platform' ,
116- 'appletv' ,
117- 'android' ,
118- ] as const ;
119- for ( const platform of windowsPlatforms ) {
120- if ( this . imagesChecked >= MAX_IMAGES_PER_CYCLE ) break ;
121- await this . checkImage ( {
122- repository : 'unityci/editor' ,
123- tag : `windows-${ editorVersion } -${ platform } -${ this . repoVersionFull } ` ,
130+ private computeExpectedImages ( versions : EditorVersionInfo [ ] ) : DockerImage [ ] {
131+ const expected : DockerImage [ ] = [
132+ {
133+ repository : 'unityci/base' ,
134+ tag : `ubuntu-${ this . repoVersionFull } ` ,
135+ baseOs : 'ubuntu' ,
136+ imageType : 'base' ,
137+ } ,
138+ {
139+ repository : 'unityci/base' ,
140+ tag : `windows-${ this . repoVersionFull } ` ,
124141 baseOs : 'windows' ,
125- imageType : 'editor' ,
126- targetPlatform : platform ,
127- editorVersion,
128- changeset,
129- } ) ;
130- }
131- }
142+ imageType : 'base' ,
143+ } ,
144+ {
145+ repository : 'unityci/hub' ,
146+ tag : `ubuntu-${ this . repoVersionFull } ` ,
147+ baseOs : 'ubuntu' ,
148+ imageType : 'hub' ,
149+ } ,
150+ {
151+ repository : 'unityci/hub' ,
152+ tag : `windows-${ this . repoVersionFull } ` ,
153+ baseOs : 'windows' ,
154+ imageType : 'hub' ,
155+ } ,
156+ ] ;
132157
133- private async checkImage ( image : DockerImage ) : Promise < void > {
134- this . imagesChecked += 1 ;
135- try {
136- const isMissing = await this . isDockerImageMissing ( image . repository , image . tag ) ;
137- if ( ! isMissing ) {
138- logger . debug ( `OK ${ image . repository } :${ image . tag } ` ) ;
139- return ;
158+ for ( const version of versions ) {
159+ const { version : editorVersion , changeSet : changeset } = version ;
160+ for ( const platform of UBUNTU_PLATFORMS ) {
161+ expected . push ( {
162+ repository : 'unityci/editor' ,
163+ tag : `ubuntu-${ editorVersion } -${ platform } -${ this . repoVersionFull } ` ,
164+ baseOs : 'ubuntu' ,
165+ imageType : 'editor' ,
166+ targetPlatform : platform ,
167+ editorVersion,
168+ changeset,
169+ } ) ;
170+ }
171+ for ( const platform of WINDOWS_PLATFORMS ) {
172+ expected . push ( {
173+ repository : 'unityci/editor' ,
174+ tag : `windows-${ editorVersion } -${ platform } -${ this . repoVersionFull } ` ,
175+ baseOs : 'windows' ,
176+ imageType : 'editor' ,
177+ targetPlatform : platform ,
178+ editorVersion,
179+ changeset,
180+ } ) ;
140181 }
141- logger . warn ( `Missing: ${ image . repository } :${ image . tag } ` ) ;
142- const dispatchedRetry = await this . dispatchRetry ( image ) ;
143- this . missingImages . push ( { image, dispatchedRetry } ) ;
144- } catch ( error ) {
145- this . missingImages . push ( {
146- image,
147- dispatchedRetry : false ,
148- error : String ( error ) ,
149- } ) ;
150182 }
183+
184+ return expected ;
151185 }
152186
153187 private async dispatchRetry ( image : DockerImage ) : Promise < boolean > {
154188 try {
155189 const eventType = this . getEventType ( image ) ;
156- const payload : Record < string , any > = {
157- jobId : `reconciliation-${ Date . now ( ) } - ${ image . imageType } -${ image . tag } ` ,
190+ const payload : Record < string , unknown > = {
191+ jobId : `reconciliation-${ image . imageType } -${ image . tag } ` ,
158192 repoVersionFull : this . repoVersionFull ,
159193 repoVersionMinor : this . repoVersionMinor ,
160194 repoVersionMajor : this . repoVersionMajor ,
@@ -173,7 +207,7 @@ export class DockerImageReconciler {
173207
174208 return response . status >= 200 && response . status < 300 ;
175209 } catch ( error ) {
176- logger . error ( ' Error dispatching' , error ) ;
210+ logger . error ( ` Error dispatching retry for ${ image . tag } ` , error ) ;
177211 return false ;
178212 }
179213 }
@@ -194,18 +228,23 @@ export class DockerImageReconciler {
194228 : GitHubWorkflow . eventTypes . retryWindowsEditorImage ;
195229 }
196230
197- private async reportResults ( ) : Promise < void > {
231+ private async reportResults ( expectedCount : number ) : Promise < void > {
198232 if ( this . missingImages . length === 0 ) {
199- await Discord . sendDebug ( `[DockerImageReconciler] Checked ${ this . imagesChecked } images OK` ) ;
233+ await Discord . sendDebug (
234+ `[DockerImageReconciler] Verified ${ expectedCount } expected images, all present` ,
235+ ) ;
200236 return ;
201237 }
202238
203239 const successful = this . missingImages . filter ( ( m ) => m . dispatchedRetry ) . length ;
204- const failedCount = this . missingImages . length - successful ;
240+ const failed = this . missingImages . length - successful ;
205241
206242 await Discord . sendAlert (
207- `DockerHub Reconciliation: Found ${ this . missingImages . length } missing, ` +
208- `retried ${ successful } , failed ${ failedCount } ` ,
243+ `DockerHub Reconciliation: ${ this . missingImages . length } missing of ${ expectedCount } expected; ` +
244+ `retried ${ successful } , failed ${ failed } ` +
245+ ( this . retriesDispatched >= MAX_RETRIES_PER_CYCLE
246+ ? ` (capped at ${ MAX_RETRIES_PER_CYCLE } retries; remaining will retry next cycle)`
247+ : '' ) ,
209248 ) ;
210249 }
211250}
0 commit comments