feat: hold the update floor above what the package managers accept - #29
Merged
Conversation
The shared preset set no minimumReleaseAge, so Renovate proposed a dependency the moment it was published. pnpm 11 refuses one that new. The two settings had never been compared, and they disagree in a way that surfaces far from either. ─── Why it matters more than it looks ─── pnpm 11 defaults minimumReleaseAge to 1440 minutes, and — the part that makes this sharp — it re-verifies EVERY entry in the lockfile on every install, not just the entries being added. A single package younger than the floor anywhere in the tree fails `pnpm install --frozen-lockfile`, so the red lands on whatever pull request happens to run next, having changed nothing related to it. That means the disagreement does not fail where it is caused. It fails somewhere else, later, on someone else's change. ─── The floor ─── minimumReleaseAge is three days: above pnpm's one, with headroom, and free against the weekly schedule the preset already runs on. It is also worth having on its own terms — a compromised publish is usually caught within hours, and nothing here needs to be the first consumer of a release. vulnerabilityAlerts states minimumReleaseAge: null rather than inheriting it. Renovate already defaults that way; writing it down means a later edit to the top-level floor cannot quietly start delaying CVE fixes. A security PR whose fix is itself hours old may still be refused by pnpm until it ages out — opening it immediately is the point, and it goes green on a re-run. ─── A check, because the validator cannot do this one ─── renovate-config-validator checks shape. It accepts `minimumReleaseAge: "3 bananas"` without complaint, because the schema says "string" and stops there, and it has no opinion on whether the value agrees with the tools that consume the lockfiles Renovate writes. scripts/check-renovate-floor.py asserts what the schema cannot: that the floor parses as a duration, that it is at or above pnpm's, and that the security path is explicitly exempt. `--self-test` breaks the config nine ways and fails unless every break is rejected, with the shipped config as the control — a gate nobody has watched fail is not known to be a gate. Both run in CI, alongside the validator in --strict mode. default.json is consumed by every repository in the org and until now nothing read it before it shipped. The new job joins the merge gate's needs, so it cannot be added and then quietly ignored.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two settings that were never compared
The shared preset set no
minimumReleaseAge, so Renovate proposed a dependency the moment it was published. pnpm 11 refuses one that new. Nothing had ever put those two facts side by side.Why this fails in a nasty place
pnpm 11 defaults
minimumReleaseAgeto 1440 minutes, and — the part that makes it sharp — re-verifies every entry in the lockfile on every install, not only the entries being added:So one too-young package anywhere in a tree fails
pnpm install --frozen-lockfilefor the whole repository. The red lands on whichever pull request runs next, having changed nothing related to it. The disagreement does not fail where it is caused — it fails somewhere else, later, on someone else's change.The floor
minimumReleaseAge: "3 days"— above pnpm's one day with headroom, and free against the weekly schedule this preset already runs on. Worth having on its own terms too: a compromised publish is usually caught within hours, and nothing here needs to be the first consumer of a release.vulnerabilityAlertsstatesminimumReleaseAge: nullrather than inheriting it. Renovate already defaults that way; writing it down means a later edit to the top-level floor cannot quietly start delaying CVE fixes.A security PR whose fix is itself hours old may still be refused by pnpm until it ages out. Opening it immediately is the point — the PR is the signal — and it goes green on a re-run with no config change.
A check, because the validator cannot do this one
renovate-config-validatorchecks shape. It accepts this without complaint:The schema says "string" and stops there. It also has no opinion on whether the value agrees with the tools that consume the lockfiles Renovate writes — which is the invariant that actually matters.
scripts/check-renovate-floor.pyasserts what the schema cannot: the floor parses as a duration, it is at or above pnpm's, and the security path is explicitly exempt.--self-testbreaks the config nine ways and fails unless every break is rejected, with the shipped config as a control so the suite cannot pass by rejecting everything:CI
Both run in a new job, alongside the validator in
--strictmode (verified to exit 1 on a bad key, 0 on this file).default.jsonis consumed by every repository in the org and until now nothing read it before it shipped.The new job is added to the merge gate's
needs, so it cannot be added and then quietly ignored — the gate refuses any workflow containing a job it does not watch.