@@ -1314,6 +1314,175 @@ export class RegistryClient {
13141314 return null
13151315 }
13161316
1317+ /**
1318+ * Get the release date of a specific package version from registry
1319+ */
1320+ async getPackageVersionReleaseDate ( packageName : string , version : string ) : Promise < Date | null > {
1321+ try {
1322+ // First try using bun info to get package metadata
1323+ const result = await this . runCommand ( 'bun' , [ 'info' , `${ packageName } @${ version } ` , '--json' ] )
1324+ const data = JSON . parse ( result )
1325+
1326+ // Check if the package data has time information
1327+ if ( data . time && typeof data . time === 'string' ) {
1328+ return new Date ( data . time )
1329+ }
1330+
1331+ // If bun doesn't provide time info, fall back to npm registry API
1332+ if ( ! data . time ) {
1333+ try {
1334+ const npmResult = await this . runCommand ( 'npm' , [ 'view' , `${ packageName } @${ version } ` , 'time' , '--json' ] )
1335+ const timeData = JSON . parse ( npmResult )
1336+
1337+ if ( timeData && typeof timeData === 'string' ) {
1338+ return new Date ( timeData )
1339+ }
1340+ }
1341+ catch ( npmError ) {
1342+ this . logger . debug ( `npm fallback failed for ${ packageName } @${ version } :` , npmError )
1343+ }
1344+ }
1345+
1346+ return null
1347+ }
1348+ catch ( error ) {
1349+ this . logger . debug ( `Failed to get release date for ${ packageName } @${ version } :` , error )
1350+ return null
1351+ }
1352+ }
1353+
1354+ /**
1355+ * Get the release date for GitHub Actions
1356+ */
1357+ async getGitHubActionReleaseDate ( actionName : string , version : string ) : Promise < Date | null > {
1358+ try {
1359+ // GitHub Actions use GitHub releases API
1360+ // Format: owner/repo@version
1361+ const [ owner , repo ] = actionName . split ( '/' )
1362+ if ( ! owner || ! repo ) {
1363+ return null
1364+ }
1365+
1366+ // Use GitHub API to get release information
1367+ const apiUrl = `https://api.github.com/repos/${ owner } /${ repo } /releases/tags/${ version } `
1368+
1369+ // Use curl to fetch the release data
1370+ const result = await this . runCommand ( 'curl' , [ '-s' , '-H' , 'Accept: application/vnd.github.v3+json' , apiUrl ] )
1371+ const releaseData = JSON . parse ( result )
1372+
1373+ if ( releaseData . published_at ) {
1374+ return new Date ( releaseData . published_at )
1375+ }
1376+
1377+ return null
1378+ }
1379+ catch ( error ) {
1380+ this . logger . debug ( `Failed to get GitHub Action release date for ${ actionName } @${ version } :` , error )
1381+ return null
1382+ }
1383+ }
1384+
1385+ /**
1386+ * Get the release date for Composer packages
1387+ */
1388+ async getComposerPackageReleaseDate ( packageName : string , version : string ) : Promise < Date | null > {
1389+ try {
1390+ // Use Packagist API for Composer packages
1391+ const apiUrl = `https://packagist.org/packages/${ packageName } .json`
1392+
1393+ const result = await this . runCommand ( 'curl' , [ '-s' , apiUrl ] )
1394+ const packageData = JSON . parse ( result )
1395+
1396+ if ( packageData . package && packageData . package . versions && packageData . package . versions [ version ] ) {
1397+ const versionData = packageData . package . versions [ version ]
1398+ if ( versionData . time ) {
1399+ return new Date ( versionData . time )
1400+ }
1401+ }
1402+
1403+ return null
1404+ }
1405+ catch ( error ) {
1406+ this . logger . debug ( `Failed to get Composer package release date for ${ packageName } @${ version } :` , error )
1407+ return null
1408+ }
1409+ }
1410+
1411+ /**
1412+ * Get the release date for Docker images
1413+ */
1414+ async getDockerImageReleaseDate ( imageName : string , version : string ) : Promise < Date | null > {
1415+ try {
1416+ // For Docker Hub images, we can use the Docker Hub API
1417+ // This is more complex as it requires authentication for some images
1418+ // For now, we'll be conservative and allow Docker image updates
1419+ this . logger . debug ( `Docker image release date checking not fully implemented for ${ imageName } :${ version } ` )
1420+ return null
1421+ }
1422+ catch ( error ) {
1423+ this . logger . debug ( `Failed to get Docker image release date for ${ imageName } :${ version } :` , error )
1424+ return null
1425+ }
1426+ }
1427+
1428+ /**
1429+ * Check if a package version meets the minimum release age requirement
1430+ */
1431+ async meetsMinimumReleaseAge ( packageName : string , version : string , dependencyType ?: string ) : Promise < boolean > {
1432+ const minimumReleaseAge = this . config ?. packages ?. minimumReleaseAge ?? 0
1433+ const excludeList = this . config ?. packages ?. minimumReleaseAgeExclude ?? [ ]
1434+
1435+ // If no minimum age is set, allow all packages
1436+ if ( minimumReleaseAge === 0 ) {
1437+ return true
1438+ }
1439+
1440+ // If package is in exclude list, allow it immediately
1441+ if ( excludeList . includes ( packageName ) ) {
1442+ this . logger . debug ( `Package ${ packageName } is excluded from minimum release age requirement` )
1443+ return true
1444+ }
1445+
1446+ // Get the release date based on dependency type
1447+ let releaseDate : Date | null = null
1448+
1449+ switch ( dependencyType ) {
1450+ case 'github-actions' :
1451+ releaseDate = await this . getGitHubActionReleaseDate ( packageName , version )
1452+ break
1453+ case 'require' :
1454+ case 'require-dev' :
1455+ releaseDate = await this . getComposerPackageReleaseDate ( packageName , version )
1456+ break
1457+ case 'docker-image' :
1458+ releaseDate = await this . getDockerImageReleaseDate ( packageName , version )
1459+ break
1460+ default :
1461+ // For npm/bun packages (dependencies, devDependencies, etc.)
1462+ releaseDate = await this . getPackageVersionReleaseDate ( packageName , version )
1463+ break
1464+ }
1465+
1466+ if ( ! releaseDate ) {
1467+ // If we can't get the release date, be conservative and allow the update
1468+ // This prevents blocking updates when registry data is unavailable
1469+ this . logger . warn ( `Could not determine release date for ${ packageName } @${ version } (${ dependencyType || 'unknown type' } ), allowing update` )
1470+ return true
1471+ }
1472+
1473+ // Calculate age in minutes
1474+ const now = new Date ( )
1475+ const ageInMinutes = ( now . getTime ( ) - releaseDate . getTime ( ) ) / ( 1000 * 60 )
1476+
1477+ const meetsRequirement = ageInMinutes >= minimumReleaseAge
1478+
1479+ if ( ! meetsRequirement ) {
1480+ this . logger . info ( `Package ${ packageName } @${ version } (${ dependencyType || 'unknown type' } ) is too new (${ Math . round ( ageInMinutes ) } minutes old, minimum: ${ minimumReleaseAge } minutes)` )
1481+ }
1482+
1483+ return meetsRequirement
1484+ }
1485+
13171486 /**
13181487 * Run bun outdated for a specific workspace
13191488 */
0 commit comments