diff --git a/bin/dependencies/currency/update-currencies.js b/bin/dependencies/currency/update-currencies.js index 3993ba01a3..0b8ff61137 100644 --- a/bin/dependencies/currency/update-currencies.js +++ b/bin/dependencies/currency/update-currencies.js @@ -60,7 +60,7 @@ currencies.forEach(originalCurrency => { return; } - const latestVersion = utils.getLatestVersion({ + let latestVersion = utils.getLatestVersion({ pkgName: currency.name, installedVersion, isBeta: currency.isBeta @@ -71,6 +71,18 @@ currencies.forEach(originalCurrency => { return; } + latestVersion = utils.getDelayedLatestVersion({ + pkgName: currency.name, + installedVersion, + upperBoundVersion: latestVersion, + isBeta: currency.isBeta + }); + + if (latestVersion === installedVersion) { + console.log(`[SKIP] ${currency.name}: no version older than ${utils.MIN_VERSION_AGE_DAYS} days available.`); + return; + } + const isMajorUpdate = semver.major(latestVersion) !== semver.major(installedVersion); console.log(`[UPDATE] ${currency.name}: ${installedVersion} → ${latestVersion}`); diff --git a/bin/dependencies/production/update-prod-dependencies.js b/bin/dependencies/production/update-prod-dependencies.js index 1558f55544..88d863cdab 100644 --- a/bin/dependencies/production/update-prod-dependencies.js +++ b/bin/dependencies/production/update-prod-dependencies.js @@ -55,7 +55,7 @@ Object.entries(dependencyMap).some(([dep, usageList]) => { } const currentVersion = utils.cleanVersionString(usageList[0].version); - const latestVersion = utils.getLatestVersion({ + let latestVersion = utils.getLatestVersion({ pkgName: dep, installedVersion: currentVersion, isBeta: false @@ -63,6 +63,18 @@ Object.entries(dependencyMap).some(([dep, usageList]) => { if (!latestVersion || latestVersion === currentVersion) return false; + latestVersion = utils.getDelayedLatestVersion({ + pkgName: dep, + installedVersion: currentVersion, + upperBoundVersion: latestVersion, + isBeta: false + }); + + if (latestVersion === currentVersion) { + console.log(`Skipping ${dep}. No version older than ${utils.MIN_VERSION_AGE_DAYS} days available.`); + return false; + } + const branchName = utils.createBranchName(BRANCH, dep, latestVersion); console.log(`Preparing PR for ${dep} (${currentVersion} -> ${latestVersion})`); diff --git a/bin/dependencies/test/utils.test.js b/bin/dependencies/test/utils.test.js index 6c48247b87..78e333dd69 100644 --- a/bin/dependencies/test/utils.test.js +++ b/bin/dependencies/test/utils.test.js @@ -5,7 +5,7 @@ 'use strict'; const path = require('path'); -const { getDaysBehind, installPackage } = require('../utils'); +const { getDaysBehind, getDelayedLatestVersion, installPackage } = require('../utils'); const assert = require('assert'); const today = '2024-07-23T00:00:00.000Z'; @@ -122,6 +122,79 @@ try { assert.strictEqual(daysBehind6, 136); console.log('Test 6 passed: Integration version is 136 days behind the latest version'); + const npmTimeOutput = { + created: '2023-01-01T00:00:00.000Z', + modified: '2024-07-22T00:00:00.000Z', + '1.0.0': '2024-01-01T00:00:00.000Z', + '1.1.0': '2024-05-10T00:00:00.000Z', + '1.2.0': '2024-07-15T00:00:00.000Z', + '1.2.1': '2024-07-20T00:00:00.000Z', + '1.2.2': '2024-07-22T00:00:00.000Z', + '2.0.0-beta.1': '2024-07-10T00:00:00.000Z' + }; + const mockNpmView = () => Buffer.from(JSON.stringify(npmTimeOutput)); + const delayedToday = new Date('2024-07-23T00:00:00.000Z'); + + const delayed1 = getDelayedLatestVersion({ + pkgName: 'some-pkg', + installedVersion: '1.1.0', + upperBoundVersion: '1.2.2', + isBeta: false, + minAgeDays: 5, + today: delayedToday, + execSyncFn: mockNpmView + }); + assert.strictEqual(delayed1, '1.2.0'); + console.log('Test passed: Picks the highest version that is at least 5 days old.'); + + const delayed2 = getDelayedLatestVersion({ + pkgName: 'some-pkg', + installedVersion: '1.2.0', + upperBoundVersion: '1.2.2', + isBeta: false, + minAgeDays: 5, + today: delayedToday, + execSyncFn: mockNpmView + }); + assert.strictEqual(delayed2, '1.2.0'); + console.log('Test passed: Returns installed version when no candidate is old enough.'); + + const delayed3 = getDelayedLatestVersion({ + pkgName: 'some-pkg', + installedVersion: '1.1.0', + upperBoundVersion: '1.2.2', + isBeta: false, + minAgeDays: 5, + today: delayedToday, + execSyncFn: mockNpmView + }); + assert.strictEqual(delayed3, '1.2.0'); + console.log('Test passed: Excludes prerelease versions when isBeta is false.'); + + const delayed4 = getDelayedLatestVersion({ + pkgName: 'some-pkg', + installedVersion: '1.1.0', + upperBoundVersion: '2.0.0-beta.1', + isBeta: true, + minAgeDays: 5, + today: delayedToday, + execSyncFn: mockNpmView + }); + assert.strictEqual(delayed4, '2.0.0-beta.1'); + console.log('Test passed: Includes prerelease versions when isBeta is true.'); + + const delayed5 = getDelayedLatestVersion({ + pkgName: 'some-pkg', + installedVersion: '1.0.0', + upperBoundVersion: '1.2.0', + isBeta: false, + minAgeDays: 5, + today: delayedToday, + execSyncFn: mockNpmView + }); + assert.strictEqual(delayed5, '1.2.0'); + console.log('Test passed: Respects upper bound (does not jump beyond it).'); + let cmd; installPackage({ cwd: path.join(__dirname, 'assets'), diff --git a/bin/dependencies/utils.js b/bin/dependencies/utils.js index f81320049d..3dbdd4d6ca 100644 --- a/bin/dependencies/utils.js +++ b/bin/dependencies/utils.js @@ -13,6 +13,35 @@ const moment = require('moment'); const { execSync } = require('child_process'); const fs = require('fs'); +const MIN_VERSION_AGE_DAYS = 5; +exports.MIN_VERSION_AGE_DAYS = MIN_VERSION_AGE_DAYS; + +exports.getDelayedLatestVersion = ({ + pkgName, + installedVersion, + upperBoundVersion, + isBeta, + minAgeDays = MIN_VERSION_AGE_DAYS, + today = new Date(), + execSyncFn = execSync +}) => { + const time = JSON.parse(execSyncFn(`npm view ${pkgName} time --json`).toString()); + const cutoff = moment(today).subtract(minAgeDays, 'days'); + + const candidates = Object.entries(time) + .filter(([version, publishedAt]) => { + if (!semver.valid(version)) return false; + if (!isBeta && semver.prerelease(version)) return false; + if (!semver.gt(version, installedVersion)) return false; + if (semver.gt(version, upperBoundVersion)) return false; + return !moment(publishedAt).isAfter(cutoff); + }) + .map(([version]) => version); + + if (candidates.length === 0) return installedVersion; + return candidates.sort(semver.rcompare)[0]; +}; + exports.getRootDependencyVersion = name => { const pkgjson = require(path.join(__dirname, '..', '..', 'package.json')); return pkgjson.devDependencies[name] || pkgjson.optionalDependencies[name];