diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index f44b2ad0db4ca7..30393d53a4892d 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -1518,14 +1518,14 @@ Change this setting to `true` if you want to use internal Renovate checks toward ## internalChecksFilter This setting determines whether Renovate controls when and how filtering of internal checks are performed, particularly when multiple versions of the same update type are available. -Currently this applies to the `stabilityDays` check only. +Currently this applies to the `minimumReleaseAge` check only. - `none`: No filtering will be performed, and the highest release will be used regardless of whether it's pending or not - `strict`: All pending releases will be filtered. PRs will be skipped unless a non-pending version is available - `flexible`: Similar to strict, but in the case where all versions are pending then a PR will be created with the highest pending version -The `flexible` mode can result in "flapping" of Pull Requests, where e.g. a pending PR with version `1.0.3` is first released but then downgraded to `1.0.2` once it passes `stabilityDays`. -We recommend that you use the `strict` mode, and enable the `dependencyDashboard` so that you have visibility into suppressed PRs. +The `flexible` mode can result in "flapping" of Pull Requests, for example: a pending PR with version `1.0.3` is first released but then downgraded to `1.0.2` once it passes `minimumReleaseAge`. +We recommend that you use the `strict` mode, and enable the `dependencyDashboard` so that you can see suppressed PRs. ## java @@ -1608,6 +1608,60 @@ Depending on its running schedule, Renovate may run a few times within that time Add to this object if you wish to define rules that apply only to major updates. +## minimumReleaseAge + +If this is set _and_ an update has a release timestamp header, then Renovate will check if the set duration has passed. + +Note: Renovate will wait for the set duration to pass for each **separate** version. +Renovate does not wait until the package has seen no releases for x time-duration(`minimumReleaseAge`). +`minimumReleaseAge` is not intended to help with slowing down fast releasing project updates. +If you want to slow down PRs for a specific package, setup a custom schedule for that package. +Read [our selective-scheduling help](https://docs.renovatebot.com/noise-reduction/#selective-scheduling) to learn how to set the schedule. + +If the time since the release is less than the set `minimumReleaseAge` a "pending" status check is added to the branch. +If enough days have passed then the "pending" status is removed, and a "passing" status check is added. + +Some datasources don't have a release timestamp, in which case this feature is not compatible. +Other datasources may have a release timestamp, but Renovate does not support it yet, in which case a feature request needs to be implemented. + +Maven users: you cannot use `minimumReleaseAge` if a Maven source returns unreliable `last-modified` headers. + + +!!! note + Configuring this option will add a `renovate/stability-days` option to the status checks. + +There are a couple of uses for `minimumReleaseAge`: + + + +#### Suppress branch/PR creation for X days + +If you combine `minimumReleaseAge=3 days` and `internalChecksFilter="strict"` then Renovate will hold back from creating branches until 3 or more days have elapsed since the version was released. +We recommend that you set `dependencyDashboard=true` so you can see these pending PRs. + +#### Prevent holding broken npm packages + +npm packages less than 72 hours (3 days) old can be unpublished, which could result in a service impact if you have already updated to it. +Set `minimumReleaseAge` to `3 days` for npm packages to prevent relying on a package that can be removed from the registry: + +```json +{ + "packageRules": [ + { + "matchDatasources": ["npm"], + "minimumReleaseAge": "3 days" + } + ] +} +``` + +#### Await X time duration before Automerging + +If you enabled `automerge` _and_ `minimumReleaseAge`, it means that PRs will be created immediately but automerging will be delayed until the time-duration has passed. +This works because Renovate will add a "renovate/stability-days" pending status check to each branch/PR and that pending check will prevent the branch going green to automerge. + + + ## minor Add to this object if you wish to define rules that apply only to minor updates. @@ -2583,7 +2637,7 @@ This is why we configured an upper limit for how long we wait until creating a P !!! note - If the option `stabilityDays` is non-zero then Renovate disables the `prNotPendingHours` functionality. + If the option `minimumReleaseAge` is non-zero then Renovate disables the `prNotPendingHours` functionality. ## prPriority @@ -3192,55 +3246,6 @@ Configure this to `true` if you wish to get one PR for every separate major vers e.g. if you are on webpack@v1 currently then default behavior is a PR for upgrading to webpack@v3 and not for webpack@v2. If this setting is true then you would get one PR for webpack@v2 and one for webpack@v3. -## stabilityDays - -If this is set to a non-zero value, _and_ an update has a release timestamp header, then Renovate will check if the "stability days" have passed. - -Note: Renovate will wait for the set number of `stabilityDays` to pass for each **separate** version. -Renovate does not wait until the package has seen no releases for x `stabilityDays`. -`stabilityDays` is not intended to help with slowing down fast releasing project updates. -If you want to slow down PRs for a specific package, setup a custom schedule for that package. -Read [our selective-scheduling help](https://docs.renovatebot.com/noise-reduction/#selective-scheduling) to learn how to set the schedule. - -If the number of days since the release is less than the set `stabilityDays` a "pending" status check is added to the branch. -If enough days have passed then the "pending" status is removed, and a "passing" status check is added. - -Some datasources do not provide a release timestamp (in which case this feature is not compatible), and other datasources may provide a release timestamp but it's not supported by Renovate (in which case a feature request needs to be implemented). - -Maven users: you cannot use `stabilityDays` if a Maven source returns unreliable `last-modified` headers. - -There are a couple of uses for `stabilityDays`: - - - -#### Suppress branch/PR creation for X days - -If you combine `stabilityDays=3` and `internalChecksFilter="strict"` then Renovate will hold back from creating branches until 3 or more days have elapsed since the version was released. -It's recommended that you enable `dependencyDashboard=true` so you don't lose visibility of these pending PRs. - -#### Prevent holding broken npm packages - -npm packages less than 72 hours (3 days) old can be unpublished, which could result in a service impact if you have already updated to it. -Set `stabilityDays` to 3 for npm packages to prevent relying on a package that can be removed from the registry: - -```json -{ - "packageRules": [ - { - "matchDatasources": ["npm"], - "stabilityDays": 3 - } - ] -} -``` - -#### Await X days before Automerging - -If you have both `automerge` as well as `stabilityDays` enabled, it means that PRs will be created immediately but automerging will be delayed until X days have passed. -This works because Renovate will add a "renovate/stability-days" pending status check to each branch/PR and that pending check will prevent the branch going green to automerge. - - - ## stopUpdatingLabel This feature only works on supported platforms, check the table above. diff --git a/lib/config/migrations/custom/stability-days-migration.spec.ts b/lib/config/migrations/custom/stability-days-migration.spec.ts new file mode 100644 index 00000000000000..b13719bb3901ea --- /dev/null +++ b/lib/config/migrations/custom/stability-days-migration.spec.ts @@ -0,0 +1,30 @@ +import { StabilityDaysMigration } from './stability-days-migration'; + +describe('config/migrations/custom/stability-days-migration', () => { + it('migrates', () => { + expect(StabilityDaysMigration).toMigrate( + { + stabilityDays: 0, + }, + { + minimumReleaseAge: null, + } + ); + expect(StabilityDaysMigration).toMigrate( + { + stabilityDays: 2, + }, + { + minimumReleaseAge: '2 days', + } + ); + expect(StabilityDaysMigration).toMigrate( + { + stabilityDays: 1, + }, + { + minimumReleaseAge: '1 day', + } + ); + }); +}); diff --git a/lib/config/migrations/custom/stability-days-migration.ts b/lib/config/migrations/custom/stability-days-migration.ts new file mode 100644 index 00000000000000..49d86cdabf80ea --- /dev/null +++ b/lib/config/migrations/custom/stability-days-migration.ts @@ -0,0 +1,25 @@ +import is from '@sindresorhus/is'; +import { AbstractMigration } from '../base/abstract-migration'; + +export class StabilityDaysMigration extends AbstractMigration { + override readonly deprecated = true; + override readonly propertyName = 'stabilityDays'; + + override run(value: unknown): void { + if (is.integer(value)) { + let newValue: null | string; + switch (value) { + case 0: + newValue = null; + break; + case 1: + newValue = '1 day'; + break; + default: + newValue = `${value} days`; + break; + } + this.setSafely('minimumReleaseAge', newValue); + } + } +} diff --git a/lib/config/migrations/migrations-service.ts b/lib/config/migrations/migrations-service.ts index de172fdd7c391b..08177481823d1f 100644 --- a/lib/config/migrations/migrations-service.ts +++ b/lib/config/migrations/migrations-service.ts @@ -47,6 +47,7 @@ import { SemanticCommitsMigration } from './custom/semantic-commits-migration'; import { SemanticPrefixMigration } from './custom/semantic-prefix-migration'; import { SeparateMajorReleasesMigration } from './custom/separate-major-release-migration'; import { SeparateMultipleMajorMigration } from './custom/separate-multiple-major-migration'; +import { StabilityDaysMigration } from './custom/stability-days-migration'; import { SuppressNotificationsMigration } from './custom/suppress-notifications-migration'; import { TrustLevelMigration } from './custom/trust-level-migration'; import { UnpublishSafeMigration } from './custom/unpublish-safe-migration'; @@ -143,6 +144,7 @@ export class MigrationsService { SemanticPrefixMigration, MatchDatasourcesMigration, DatasourceMigration, + StabilityDaysMigration, ]; static run(originalConfig: RenovateConfig): RenovateConfig { diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index faa0653fd50761..233ba616f235a9 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1576,16 +1576,15 @@ const options: RenovateOptions[] = [ supportedPlatforms: ['azure', 'gitea', 'github', 'gitlab'], }, { - name: 'stabilityDays', - description: - 'Number of days required before a new release is considered stable.', - type: 'integer', - default: 0, + name: 'minimumReleaseAge', + description: 'Time required before a new release is considered stable.', + type: 'string', + default: null, }, { name: 'internalChecksAsSuccess', description: - 'Whether to consider passing internal checks such as stabilityDays when determining branch status.', + 'Whether to consider passing internal checks such as `minimumReleaseAge` when determining branch status.', type: 'boolean', default: false, }, @@ -1719,7 +1718,7 @@ const options: RenovateOptions[] = [ groupName: null, schedule: [], dependencyDashboardApproval: false, - stabilityDays: 0, + minimumReleaseAge: null, rangeStrategy: 'update-lockfile', commitMessageSuffix: '[SECURITY]', branchTopic: `{{{datasource}}}-{{{depName}}}-vulnerability`, diff --git a/lib/config/presets/internal/npm.ts b/lib/config/presets/internal/npm.ts index 57552b194e38e9..e269bbd8c77072 100644 --- a/lib/config/presets/internal/npm.ts +++ b/lib/config/presets/internal/npm.ts @@ -7,7 +7,7 @@ export const presets: Record = { description: 'Wait until the npm package is three days old before raising the update, this prevents npm unpublishing a package you already upgraded to.', npm: { - stabilityDays: 3, + minimumReleaseAge: '3 days', }, }, }; diff --git a/lib/util/date.spec.ts b/lib/util/date.spec.ts index 87d3742ac28923..08d5d102c38898 100644 --- a/lib/util/date.spec.ts +++ b/lib/util/date.spec.ts @@ -1,4 +1,9 @@ -import { getElapsedDays, getElapsedHours, getElapsedMinutes } from './date'; +import { + getElapsedDays, + getElapsedHours, + getElapsedMinutes, + getElapsedMs, +} from './date'; const ONE_MINUTE_MS = 60 * 1000; const ONE_HOUR_MS = 60 * ONE_MINUTE_MS; @@ -34,4 +39,11 @@ describe('util/date', () => { expect(getElapsedHours(new Date('invalid_date_string'))).toBe(0); }); }); + + describe('getElapsedMilliseconds', () => { + it('returns elapsed time in milliseconds', () => { + const elapsedMs = new Date().getTime() - new Date(Jan1).getTime(); + expect(getElapsedMs(Jan1.toISOString())).toBe(elapsedMs); + }); + }); }); diff --git a/lib/util/date.ts b/lib/util/date.ts index 53d7877da52dc5..61fbe64b5c8f40 100644 --- a/lib/util/date.ts +++ b/lib/util/date.ts @@ -26,3 +26,7 @@ export function getElapsedHours(date: Date | string): number { const diff = DateTime.now().diff(pastDate, 'hours'); return Math.floor(diff.hours); } + +export function getElapsedMs(timestamp: string): number { + return new Date().getTime() - new Date(timestamp).getTime(); +} diff --git a/lib/util/pretty-time.spec.ts b/lib/util/pretty-time.spec.ts index a9f291e467a9c6..266089e7e03c29 100644 --- a/lib/util/pretty-time.spec.ts +++ b/lib/util/pretty-time.spec.ts @@ -15,6 +15,8 @@ describe('util/pretty-time', () => { ${'1h 1 m 1s'} | ${1 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1000} ${'1hour 1 min 1s'} | ${1 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1000} ${'1h 1m 1s 1ms'} | ${1 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1000 + 1} + ${'1d2h3m'} | ${24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000 + 3 * 60 * 1000} + ${'1 day'} | ${24 * 60 * 60 * 1000} ${'3 days'} | ${3 * 24 * 60 * 60 * 1000} ${'1 week'} | ${7 * 24 * 60 * 60 * 1000} ${'1 month'} | ${30 * 24 * 60 * 60 * 1000} diff --git a/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap b/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap index 990d099897ce2e..e731e34f30e745 100644 --- a/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap +++ b/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap @@ -21,10 +21,10 @@ exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() retur "commitMessageSuffix": "[SECURITY]", "dependencyDashboardApproval": false, "groupName": null, + "minimumReleaseAge": null, "prCreation": "immediate", "rangeStrategy": "update-lockfile", "schedule": [], - "stabilityDays": 0, }, "isVulnerabilityAlert": true, "matchCurrentVersion": "= 1.8.2", @@ -51,10 +51,10 @@ go", "commitMessageSuffix": "[SECURITY]", "dependencyDashboardApproval": false, "groupName": null, + "minimumReleaseAge": null, "prCreation": "immediate", "rangeStrategy": "update-lockfile", "schedule": [], - "stabilityDays": 0, }, "isVulnerabilityAlert": true, "matchCurrentVersion": "1.8.2", @@ -81,10 +81,10 @@ actions", "commitMessageSuffix": "[SECURITY]", "dependencyDashboardApproval": false, "groupName": null, + "minimumReleaseAge": null, "prCreation": "immediate", "rangeStrategy": "update-lockfile", "schedule": [], - "stabilityDays": 0, }, "isVulnerabilityAlert": true, "matchCurrentVersion": "== 1.6.7", @@ -126,10 +126,10 @@ Ansible before versions 2.1.4, 2.2.1 is vulnerable to an improper input validati "commitMessageSuffix": "[SECURITY]", "dependencyDashboardApproval": false, "groupName": null, + "minimumReleaseAge": null, "prCreation": "immediate", "rangeStrategy": "update-lockfile", "schedule": [], - "stabilityDays": 0, }, "isVulnerabilityAlert": true, "matchCurrentVersion": "2.4.2", diff --git a/lib/workers/repository/process/lookup/__snapshots__/filter-checks.spec.ts.snap b/lib/workers/repository/process/lookup/__snapshots__/filter-checks.spec.ts.snap index 3c5cc28de8b003..5bf232ab929a17 100644 --- a/lib/workers/repository/process/lookup/__snapshots__/filter-checks.spec.ts.snap +++ b/lib/workers/repository/process/lookup/__snapshots__/filter-checks.spec.ts.snap @@ -24,7 +24,7 @@ exports[`workers/repository/process/lookup/filter-checks .filterInternalChecks() } `; -exports[`workers/repository/process/lookup/filter-checks .filterInternalChecks() picks up stabilityDays settings from hostRules 1`] = ` +exports[`workers/repository/process/lookup/filter-checks .filterInternalChecks() picks up minimumReleaseAge settings from hostRules 1`] = ` { "pendingChecks": false, "pendingReleases": [], @@ -35,7 +35,7 @@ exports[`workers/repository/process/lookup/filter-checks .filterInternalChecks() } `; -exports[`workers/repository/process/lookup/filter-checks .filterInternalChecks() picks up stabilityDays settings from updateType 1`] = ` +exports[`workers/repository/process/lookup/filter-checks .filterInternalChecks() picks up minimumReleaseAge settings from updateType 1`] = ` { "pendingChecks": false, "pendingReleases": [ diff --git a/lib/workers/repository/process/lookup/filter-checks.spec.ts b/lib/workers/repository/process/lookup/filter-checks.spec.ts index 5802aced0e1fe6..8c210db87d6526 100644 --- a/lib/workers/repository/process/lookup/filter-checks.spec.ts +++ b/lib/workers/repository/process/lookup/filter-checks.spec.ts @@ -4,6 +4,7 @@ import * as allVersioning from '../../../../modules/versioning'; import { clone } from '../../../../util/clone'; import * as _dateUtil from '../../../../util/date'; import * as _mergeConfidence from '../../../../util/merge-confidence'; +import { toMs } from '../../../../util/pretty-time'; import { filterInternalChecks } from './filter-checks'; import type { LookupUpdateConfig, UpdateResult } from './types'; @@ -44,10 +45,10 @@ describe('workers/repository/process/lookup/filter-checks', () => { config.currentVersion = '1.0.0'; sortedReleases = clone(releases); jest.resetAllMocks(); - dateUtil.getElapsedDays.mockReturnValueOnce(3); - dateUtil.getElapsedDays.mockReturnValueOnce(5); - dateUtil.getElapsedDays.mockReturnValueOnce(7); - dateUtil.getElapsedDays.mockReturnValueOnce(9); + dateUtil.getElapsedMs.mockReturnValueOnce(toMs('3 days') ?? 0); + dateUtil.getElapsedMs.mockReturnValueOnce(toMs('5 days') ?? 0); + dateUtil.getElapsedMs.mockReturnValueOnce(toMs('7 days') ?? 0); + dateUtil.getElapsedMs.mockReturnValueOnce(toMs('9 days') ?? 0); }); describe('.filterInternalChecks()', () => { @@ -67,7 +68,7 @@ describe('workers/repository/process/lookup/filter-checks', () => { it('returns non-pending latest release if internalChecksFilter=flexible and none pass checks', async () => { config.internalChecksFilter = 'flexible'; - config.stabilityDays = 10; + config.minimumReleaseAge = '10 days'; const res = await filterInternalChecks( config, versioning, @@ -82,7 +83,7 @@ describe('workers/repository/process/lookup/filter-checks', () => { it('returns pending latest release if internalChecksFilter=strict and none pass checks', async () => { config.internalChecksFilter = 'strict'; - config.stabilityDays = 10; + config.minimumReleaseAge = '10 days'; const res = await filterInternalChecks( config, versioning, @@ -97,7 +98,7 @@ describe('workers/repository/process/lookup/filter-checks', () => { it('returns non-latest release if internalChecksFilter=strict and some pass checks', async () => { config.internalChecksFilter = 'strict'; - config.stabilityDays = 6; + config.minimumReleaseAge = '6 days'; const res = await filterInternalChecks( config, versioning, @@ -112,7 +113,7 @@ describe('workers/repository/process/lookup/filter-checks', () => { it('returns non-latest release if internalChecksFilter=flexible and some pass checks', async () => { config.internalChecksFilter = 'strict'; - config.stabilityDays = 6; + config.minimumReleaseAge = '6 days'; const res = await filterInternalChecks( config, versioning, @@ -125,10 +126,12 @@ describe('workers/repository/process/lookup/filter-checks', () => { expect(res.release.version).toBe('1.0.2'); }); - it('picks up stabilityDays settings from hostRules', async () => { + it('picks up minimumReleaseAge settings from hostRules', async () => { config.internalChecksFilter = 'strict'; - config.stabilityDays = 6; - config.packageRules = [{ matchUpdateTypes: ['patch'], stabilityDays: 1 }]; + config.minimumReleaseAge = '6 days'; + config.packageRules = [ + { matchUpdateTypes: ['patch'], minimumReleaseAge: '1 day' }, + ]; const res = await filterInternalChecks( config, versioning, @@ -141,9 +144,9 @@ describe('workers/repository/process/lookup/filter-checks', () => { expect(res.release.version).toBe('1.0.4'); }); - it('picks up stabilityDays settings from updateType', async () => { + it('picks up minimumReleaseAge settings from updateType', async () => { config.internalChecksFilter = 'strict'; - config.patch = { stabilityDays: 4 }; + config.patch = { minimumReleaseAge: '4 days' }; const res = await filterInternalChecks( config, versioning, diff --git a/lib/workers/repository/process/lookup/filter-checks.ts b/lib/workers/repository/process/lookup/filter-checks.ts index 4cd6ac70f6150d..83b625cffdf00f 100644 --- a/lib/workers/repository/process/lookup/filter-checks.ts +++ b/lib/workers/repository/process/lookup/filter-checks.ts @@ -3,13 +3,14 @@ import { mergeChildConfig } from '../../../../config'; import { logger } from '../../../../logger'; import type { Release } from '../../../../modules/datasource'; import type { VersioningApi } from '../../../../modules/versioning'; -import { getElapsedDays } from '../../../../util/date'; +import { getElapsedMs } from '../../../../util/date'; import { getMergeConfidenceLevel, isActiveConfidenceLevel, satisfiesConfidenceLevel, } from '../../../../util/merge-confidence'; import { applyPackageRules } from '../../../../util/package-rules'; +import { toMs } from '../../../../util/pretty-time'; import type { LookupUpdateConfig, UpdateResult } from './types'; import { getUpdateType } from './update-type'; @@ -30,7 +31,7 @@ export async function filterInternalChecks( let pendingChecks = false; let pendingReleases: Release[] = []; if (internalChecksFilter === 'none') { - // Don't care if stabilityDays or minimumConfidence are unmet + // Don't care if minimumReleaseAge or minimumConfidence are unmet release = sortedReleases.pop(); } else { // iterate through releases from highest to lowest, looking for the first which will pass checks if present @@ -51,19 +52,19 @@ export async function filterInternalChecks( ); // Apply packageRules in case any apply to updateType releaseConfig = applyPackageRules(releaseConfig); - // Now check for a stabilityDays config + // Now check for a minimumReleaseAge config const { minimumConfidence, - stabilityDays, + minimumReleaseAge, releaseTimestamp, version: newVersion, updateType, } = releaseConfig; - if (is.integer(stabilityDays) && releaseTimestamp) { - if (getElapsedDays(releaseTimestamp) < stabilityDays) { + if (is.nonEmptyString(minimumReleaseAge) && releaseTimestamp) { + if (getElapsedMs(releaseTimestamp) < (toMs(minimumReleaseAge) ?? 0)) { // Skip it if it doesn't pass checks logger.trace( - { depName, check: 'stabilityDays' }, + { depName, check: 'minimumReleaseAge' }, `Release ${candidateRelease.version} is pending status checks` ); pendingReleases.unshift(candidateRelease); diff --git a/lib/workers/repository/process/lookup/index.spec.ts b/lib/workers/repository/process/lookup/index.spec.ts index 10c963df82a8f1..fb91a5467745b3 100644 --- a/lib/workers/repository/process/lookup/index.spec.ts +++ b/lib/workers/repository/process/lookup/index.spec.ts @@ -1002,7 +1002,7 @@ describe('workers/repository/process/lookup/index', () => { config.currentValue = '1.4.4'; config.packageName = 'some/action'; config.datasource = GithubReleasesDatasource.id; - config.stabilityDays = 14; + config.minimumReleaseAge = '14 days'; config.internalChecksFilter = 'strict'; const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); @@ -1025,7 +1025,7 @@ describe('workers/repository/process/lookup/index', () => { config.currentValue = '1.4.4'; config.packageName = 'some/action'; config.datasource = GithubReleasesDatasource.id; - config.stabilityDays = 3; + config.minimumReleaseAge = '3 days'; config.internalChecksFilter = 'strict'; const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); diff --git a/lib/workers/repository/update/branch/index.spec.ts b/lib/workers/repository/update/branch/index.spec.ts index 146312255a2e52..0288741eab8ae7 100644 --- a/lib/workers/repository/update/branch/index.spec.ts +++ b/lib/workers/repository/update/branch/index.spec.ts @@ -162,17 +162,17 @@ describe('workers/repository/update/branch/index', () => { }); }); - it('skips branch for fresh release with stabilityDays', async () => { + it('skips branch for fresh release with minimumReleaseAge', async () => { schedule.isScheduledNow.mockReturnValueOnce(true); config.prCreation = 'not-pending'; (config.upgrades as Partial[]) = [ { releaseTimestamp: new Date('2019-01-01').getTime().toString(), - stabilityDays: 1, + minimumReleaseAge: '1 day', }, { releaseTimestamp: new Date().toString(), - stabilityDays: 1, + minimumReleaseAge: '1 day', }, ]; @@ -185,13 +185,13 @@ describe('workers/repository/update/branch/index', () => { }); }); - it('skips branch if not stabilityDays not met', async () => { + it('skips branch if minimumReleaseAge not met', async () => { schedule.isScheduledNow.mockReturnValueOnce(true); config.prCreation = 'not-pending'; config.upgrades = partial([ { releaseTimestamp: '2099-12-31', - stabilityDays: 1, + minimumReleaseAge: '1 day', }, ]); const res = await branchWorker.processBranch(config); diff --git a/lib/workers/repository/update/branch/index.ts b/lib/workers/repository/update/branch/index.ts index 188e5f31a1a666..077e15eacb9b9b 100644 --- a/lib/workers/repository/update/branch/index.ts +++ b/lib/workers/repository/update/branch/index.ts @@ -23,7 +23,7 @@ import { } from '../../../../modules/platform/comment'; import { scm } from '../../../../modules/platform/scm'; import { ExternalHostError } from '../../../../types/errors/external-host-error'; -import { getElapsedDays } from '../../../../util/date'; +import { getElapsedMs } from '../../../../util/date'; import { emojify } from '../../../../util/emoji'; import { checkoutBranch } from '../../../../util/git'; import { @@ -31,6 +31,7 @@ import { isActiveConfidenceLevel, satisfiesConfidenceLevel, } from '../../../../util/merge-confidence'; +import { toMs } from '../../../../util/pretty-time'; import * as template from '../../../../util/template'; import { isLimitReached } from '../../../global/limits'; import type { BranchConfig, BranchResult, PrBlockedBy } from '../../../types'; @@ -280,25 +281,29 @@ export async function processBranch( if ( config.upgrades.some( (upgrade) => - (upgrade.stabilityDays && upgrade.releaseTimestamp) || + (is.nonEmptyString(upgrade.minimumReleaseAge) && + upgrade.releaseTimestamp) || isActiveConfidenceLevel(upgrade.minimumConfidence!) ) ) { // Only set a stability status check if one or more of the updates contain - // both a stabilityDays setting and a releaseTimestamp + // both a minimumReleaseAge setting and a releaseTimestamp config.stabilityStatus = 'green'; // Default to 'success' but set 'pending' if any update is pending for (const upgrade of config.upgrades) { - if (is.number(upgrade.stabilityDays) && upgrade.releaseTimestamp) { - const daysElapsed = getElapsedDays(upgrade.releaseTimestamp); - if (daysElapsed < upgrade.stabilityDays) { + if ( + is.nonEmptyString(upgrade.minimumReleaseAge) && + upgrade.releaseTimestamp + ) { + const timeElapsed = getElapsedMs(upgrade.releaseTimestamp); + if (timeElapsed < (toMs(upgrade.minimumReleaseAge) ?? 0)) { logger.debug( { depName: upgrade.depName, - daysElapsed, - stabilityDays: upgrade.stabilityDays, + timeElapsed, + minimumReleaseAge: upgrade.minimumReleaseAge, }, - 'Update has not passed stability days' + 'Update has not passed minimum release age' ); config.stabilityStatus = 'yellow'; continue; diff --git a/lib/workers/repository/update/branch/status-checks.ts b/lib/workers/repository/update/branch/status-checks.ts index a880fc3a9e6e1e..6fe93414f0f6be 100644 --- a/lib/workers/repository/update/branch/status-checks.ts +++ b/lib/workers/repository/update/branch/status-checks.ts @@ -65,8 +65,8 @@ export async function setStability(config: StabilityConfig): Promise { const context = `renovate/stability-days`; const description = config.stabilityStatus === 'green' - ? 'Updates have met stability days requirement' - : 'Updates have not met stability days requirement'; + ? 'Updates have met minimum release age requirement' + : 'Updates have not met minimum release age requirement'; await setStatusCheck( config.branchName, context,