@@ -66,8 +66,11 @@ export class Buddy {
66
66
// Get outdated GitHub Actions
67
67
const githubActionsUpdates = await this . checkGitHubActionsForUpdates ( packageFiles )
68
68
69
+ // Get outdated Docker images
70
+ const dockerUpdates = await this . checkDockerfilesForUpdates ( packageFiles )
71
+
69
72
// Merge all updates
70
- let updates = [ ...packageJsonUpdates , ...dependencyFileUpdates , ...githubActionsUpdates ]
73
+ let updates = [ ...packageJsonUpdates , ...dependencyFileUpdates , ...githubActionsUpdates , ... dockerUpdates ]
71
74
72
75
// Apply ignore filter to dependency file updates
73
76
if ( this . config . packages ?. ignore && this . config . packages . ignore . length > 0 ) {
@@ -564,6 +567,112 @@ export class Buddy {
564
567
return deduplicatedUpdates
565
568
}
566
569
570
+ /**
571
+ * Check Dockerfiles for updates
572
+ */
573
+ private async checkDockerfilesForUpdates ( packageFiles : PackageFile [ ] ) : Promise < PackageUpdate [ ] > {
574
+ const { isDockerfile } = await import ( './utils/dockerfile-parser' )
575
+ const { fetchLatestDockerImageVersion } = await import ( './utils/dockerfile-parser' )
576
+
577
+ const updates : PackageUpdate [ ] = [ ]
578
+
579
+ // Filter to only Dockerfile files
580
+ const dockerfiles = packageFiles . filter ( file => isDockerfile ( file . path ) )
581
+
582
+ this . logger . info ( `🔍 Found ${ dockerfiles . length } Dockerfile(s)` )
583
+
584
+ for ( const file of dockerfiles ) {
585
+ try {
586
+ this . logger . info ( `Checking Dockerfile: ${ file . path } ` )
587
+
588
+ // Get all Docker image dependencies from this file
589
+ const imageDeps = file . dependencies . filter ( dep => dep . type === 'docker-image' )
590
+ this . logger . info ( `Found ${ imageDeps . length } Docker images in ${ file . path } ` )
591
+
592
+ for ( const dep of imageDeps ) {
593
+ try {
594
+ this . logger . info ( `Checking Docker image: ${ dep . name } :${ dep . currentVersion } ` )
595
+
596
+ // Check if current version should be respected (like "latest", etc.)
597
+ const shouldRespectVersion = ( version : string ) : boolean => {
598
+ const respectLatest = this . config . packages ?. respectLatest ?? true
599
+ if ( ! respectLatest )
600
+ return false
601
+
602
+ const dynamicIndicators = [ 'latest' , 'main' , 'master' , 'develop' , 'dev' , 'stable' ]
603
+ const cleanVersion = version . toLowerCase ( ) . trim ( )
604
+ return dynamicIndicators . includes ( cleanVersion )
605
+ }
606
+
607
+ if ( shouldRespectVersion ( dep . currentVersion ) ) {
608
+ this . logger . debug ( `Skipping ${ dep . name } - version "${ dep . currentVersion } " should be respected` )
609
+ continue
610
+ }
611
+
612
+ // Fetch latest version for this Docker image
613
+ const latestVersion = await fetchLatestDockerImageVersion ( dep . name )
614
+
615
+ if ( latestVersion ) {
616
+ this . logger . info ( `Latest version for ${ dep . name } : ${ latestVersion } ` )
617
+
618
+ if ( latestVersion !== dep . currentVersion ) {
619
+ // Determine update type
620
+ const updateType = this . getUpdateType ( dep . currentVersion , latestVersion )
621
+
622
+ this . logger . info ( `Update available: ${ dep . name } ${ dep . currentVersion } → ${ latestVersion } (${ updateType } )` )
623
+
624
+ updates . push ( {
625
+ name : dep . name ,
626
+ currentVersion : dep . currentVersion ,
627
+ newVersion : latestVersion ,
628
+ updateType,
629
+ dependencyType : 'docker-image' ,
630
+ file : file . path ,
631
+ metadata : undefined ,
632
+ releaseNotesUrl : `https://hub.docker.com/r/${ dep . name } /tags` ,
633
+ changelogUrl : undefined ,
634
+ homepage : `https://hub.docker.com/r/${ dep . name } ` ,
635
+ } )
636
+ }
637
+ else {
638
+ this . logger . info ( `No update needed for ${ dep . name } : already at ${ latestVersion } ` )
639
+ }
640
+ }
641
+ else {
642
+ this . logger . warn ( `Could not fetch latest version for Docker image ${ dep . name } ` )
643
+ }
644
+ }
645
+ catch ( error ) {
646
+ this . logger . warn ( `Failed to check version for Docker image ${ dep . name } :` , error )
647
+ }
648
+ }
649
+ }
650
+ catch ( error ) {
651
+ this . logger . error ( `Failed to check Dockerfile ${ file . path } :` , error )
652
+ }
653
+ }
654
+
655
+ this . logger . info ( `Generated ${ updates . length } Docker image updates` )
656
+
657
+ // Deduplicate updates by name, version, and file
658
+ const deduplicatedUpdates = updates . reduce ( ( acc , update ) => {
659
+ const existing = acc . find ( u =>
660
+ u . name === update . name
661
+ && u . currentVersion === update . currentVersion
662
+ && u . newVersion === update . newVersion
663
+ && u . file === update . file ,
664
+ )
665
+ if ( ! existing ) {
666
+ acc . push ( update )
667
+ }
668
+ return acc
669
+ } , [ ] as PackageUpdate [ ] )
670
+
671
+ this . logger . info ( `After deduplication: ${ deduplicatedUpdates . length } unique Docker image updates` )
672
+
673
+ return deduplicatedUpdates
674
+ }
675
+
567
676
/**
568
677
* Determine update type based on version comparison
569
678
*/
@@ -675,11 +784,11 @@ export class Buddy {
675
784
// This prevents accidentally updating scripts or other sections with the same package name
676
785
const escapedPackageName = cleanPackageName . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' )
677
786
const escapedSectionName = section . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' )
678
-
787
+
679
788
// Match the section, then find the package within that section
680
789
const sectionRegex = new RegExp (
681
790
`("${ escapedSectionName } "\\s*:\\s*\\{[^}]*?)("${ escapedPackageName } "\\s*:\\s*")([^"]+)(")([^}]*?\\})` ,
682
- 'gs'
791
+ 'gs' ,
683
792
)
684
793
685
794
// Preserve the original prefix when updating to new version
@@ -708,10 +817,14 @@ export class Buddy {
708
817
709
818
// Handle dependency file updates (deps.yaml, dependencies.yaml, etc.)
710
819
// Only process if we have dependency file updates to avoid unnecessary processing
711
- const dependencyFileUpdates = updates . filter ( update =>
712
- ( update . file . includes ( '.yaml' ) || update . file . includes ( '.yml' ) )
713
- && ! update . file . includes ( '.github/workflows/' ) ,
714
- )
820
+ const dependencyFileUpdates = updates . filter ( ( update ) => {
821
+ const fileName = update . file . toLowerCase ( )
822
+ const isDependencyFile = fileName . endsWith ( 'deps.yaml' )
823
+ || fileName . endsWith ( 'deps.yml' )
824
+ || fileName . endsWith ( 'dependencies.yaml' )
825
+ || fileName . endsWith ( 'dependencies.yml' )
826
+ return isDependencyFile && ! update . file . includes ( '.github/workflows/' )
827
+ } )
715
828
if ( dependencyFileUpdates . length > 0 ) {
716
829
try {
717
830
const { generateDependencyFileUpdates } = await import ( './utils/dependency-file-parser' )
@@ -761,6 +874,24 @@ export class Buddy {
761
874
}
762
875
}
763
876
877
+ // Handle Dockerfile updates
878
+ // Only process if we have Dockerfile updates to avoid unnecessary processing
879
+ const dockerfileUpdates = updates . filter ( update =>
880
+ update . dependencyType === 'docker-image' ,
881
+ )
882
+ if ( dockerfileUpdates . length > 0 ) {
883
+ try {
884
+ const { generateDockerfileUpdates } = await import ( './utils/dockerfile-parser' )
885
+ // Pass only the Dockerfile updates for this specific group
886
+ const dockerUpdates = await generateDockerfileUpdates ( dockerfileUpdates )
887
+ fileUpdates . push ( ...dockerUpdates )
888
+ }
889
+ catch ( error ) {
890
+ this . logger . error ( 'Failed to generate Dockerfile updates:' , error )
891
+ // Continue with other updates even if Dockerfile updates fail
892
+ }
893
+ }
894
+
764
895
return fileUpdates
765
896
}
766
897
0 commit comments