Skip to content

Refactor: Separate modelling of candidate newerVersions from Updates#3787

Merged
rtyley merged 1 commit intomainfrom
replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates
Jan 19, 2026
Merged

Refactor: Separate modelling of candidate newerVersions from Updates#3787
rtyley merged 1 commit intomainfrom
replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates

Conversation

@rtyley
Copy link
Contributor

@rtyley rtyley commented Jan 15, 2026

Addresses #3781 - Update.Single doesn't need newerVersions, just nextVersion

The Scala Steward codebase deals with the concept of 'updates' in two areas:

  • Multiple newerVersions: Initially, evaluating candidate updates (in UpdateAlg & FilterAlg), where for any given artifact, there can be many possible versions we might want to upgrade to. The list of versions is reduced down with filtering (based on config & other rules), and eventually a single next version is selected.
  • Single nextVersion: Subsequently, retrieving information on the updates represented by the pull requests Scala Steward created - each artifact being updated to a specific single new version.

...Scala Steward used the Update type for both areas (particularly, Update.ForArtifactId for the first phase, requiring it to support multiple newerVersions).

Looking at a single Update.ForArtifactId in code, it's not always clear to the newcomer if the update contains many newer versions or not - ie where we are in the pipeline.

Changes

  • Update.Single types (eg Update.ForArtifactId) now just have a nextVersion field, not newerVersions, which better models what they actually signify: the single specific update selected for a particular PR
  • Within UpdateAlg & FilterAlg we change Update.ForArtifactIdArtifactUpdateCandidates🆕 (which does have a newerVersions field) for the parts where multiple candidate versions are being evaluated
  • Update.ForArtifactId & ArtifactUpdateCandidates both extend ArtifactUpdateVersions🆕 to allow UpdatePattern.findMatch() to filter lists of either of them:
    • Filtering the candidate versions of ArtifactUpdateCandidates in UpdatesConfig.isAllowed, etc
    • Filtering the Updates of existing PRs in PruningAlg.newPullRequestsAllowed() & RetractedArtifact.isRetracted()
  • The fields from Update.ForArtifactId also required by ArtifactUpdateCandidates have been placed in the ArtifactForUpdate🆕 case class, avoiding possible duplication

Remaining unchanged:

  • PullRequestRepository still persists instances of Update
  • The JSON serialisation format of Update remains unchanged for now (the encoder & decoders have been adapted so that they still output the same format, despite the case classes having changed, just with a single 'newerVersion'), so there's currently no need to introduce a new fallback 'legacy' decoder (several of which were recently deleted with Remove legacy Update decoders (drop support for old PR caches) #3785). This also means the JSON metadata added to PR descriptions (originally introduced in Add JSON metadata in PRs #3466) also remains the same for now, though I think it will be reasonable to adjust the format for Update.ForArtifactId to something more logical in future.

With this PR, the changes from #3762 will no longer need to change the data stored in PullRequestRepository.

@rtyley rtyley changed the base branch from main to remove-legacy-update-decoders January 15, 2026 12:25
@rtyley rtyley force-pushed the replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates branch 5 times, most recently from 09f613e to 0489081 Compare January 15, 2026 16:47
Base automatically changed from remove-legacy-update-decoders to main January 15, 2026 17:22
@rtyley rtyley force-pushed the replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates branch 5 times, most recently from 83d4acb to 8a8d2fe Compare January 15, 2026 22:22
@rtyley rtyley force-pushed the replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates branch 3 times, most recently from 147aee0 to bd00bbb Compare January 16, 2026 07:12
@codecov
Copy link

codecov bot commented Jan 16, 2026

Codecov Report

❌ Patch coverage is 94.73684% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.88%. Comparing base (d4e87c7) to head (df28ac4).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...rg/scalasteward/core/update/data/UpdateState.scala 25.00% 3 Missing ⚠️
...main/scala/org/scalasteward/core/data/Update.scala 96.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3787      +/-   ##
==========================================
+ Coverage   89.83%   89.88%   +0.05%     
==========================================
  Files         174      174              
  Lines        5036     5063      +27     
  Branches      458      447      -11     
==========================================
+ Hits         4524     4551      +27     
  Misses        512      512              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rtyley rtyley force-pushed the replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates branch 2 times, most recently from f2bcc87 to 995c9b1 Compare January 16, 2026 09:59
@rtyley rtyley changed the title Replace update for artifact id newer versions with artifact update candidates Restrict Update.Single types to just nextVersion, not newerVersions - and avoid use of Update.ForArtifactId when evaluating candidate versions Jan 16, 2026
@rtyley rtyley force-pushed the replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates branch 9 times, most recently from 45ca0c0 to 972777f Compare January 16, 2026 13:24
rtyley added a commit that referenced this pull request Jan 16, 2026
These are some small tweaks I've extracted from this
larger PR, to take the size down a bit:

#3787
@rtyley rtyley force-pushed the replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates branch 7 times, most recently from 1e2253b to 943b62b Compare January 17, 2026 23:00
@rtyley rtyley changed the title Restrict Update.Single types to just nextVersion, not newerVersions - and avoid use of Update.ForArtifactId when evaluating candidate versions Refactor: Separate modelling of _possible_ candidate versions from Updates (where the single nextVersion has been determined) Jan 17, 2026
@rtyley rtyley changed the title Refactor: Separate modelling of _possible_ candidate versions from Updates (where the single nextVersion has been determined) Refactor: Separate modelling of candidate newerVersions from Updates (where the single nextVersion has been determined) Jan 17, 2026
@rtyley rtyley changed the title Refactor: Separate modelling of candidate newerVersions from Updates (where the single nextVersion has been determined) Refactor: Separate modelling of candidate newerVersions from Updates Jan 17, 2026
@rtyley rtyley force-pushed the replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates branch 2 times, most recently from 9f21f95 to 28c68e6 Compare January 17, 2026 23:16
@rtyley rtyley marked this pull request as ready for review January 17, 2026 23:27
Copy link
Member

@mzuehlke mzuehlke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like these more specific classes.
Everyone will benefit from easier code understanding-

_Addresses https://github.com/scala-steward-org/scala-steward/issues/3781_

The Scala Steward codebase deals with the concept of 'updates' in two areas:

* **Multiple `newerVersions`**: Initially, evaluating _candidate_ updates (in `UpdateAlg` & `FilterAlg`), where for any given artifact, there can be many possible versions we might want to upgrade to. The list of versions is reduced down with filtering (based on config & other rules), and eventually a single next version is selected.
* **Single `nextVersion`**: Subsequently, retrieving information on the updates represented by the pull requests Scala Steward created - each artifact being updated to a specific _single_ new version.

...Scala Steward used the `Update` type for _both_ areas (specifically, `Update.ForArtifactId` for the first phase, _requiring_ it to support multiple `newerVersions`).

Looking at a single `Update.ForArtifactId` in code, it's not always clear to the newcomer if the update contains many newer versions or not - ie where we are in the pipeline.

## Changes

* `Update.ForArtifactId` → `ArtifactUpdateCandidates`🆕  within `UpdateAlg` & `FilterAlg` where candidate versions are being evaluated
* `Update.ForArtifactId` & `ArtifactUpdateCandidates` both extend `ArtifactUpdateVersions`🆕 to allow `UpdatePattern.findMatch()` to filter lists of either of them:
  * Filtering the candidate versions of `ArtifactUpdateCandidates` in `UpdatesConfig.isAllowed`, etc
  * Filtering the `Update`s of existing PRs in `PruningAlg.newPullRequestsAllowed()` & `RetractedArtifact.isRetracted()`
* The fields from `Update.ForArtifactId` also required by `ArtifactUpdateCandidates` have been placed in the `ArtifactForUpdate`🆕 case class
* Restrict `Update.Single` types to just `nextVersion`, not `newerVersions`

Remaining unchanged:

* `PullRequestRepository` still persists instances of `Update`
* The JSON serialisation format of `Update` remains unchanged for now (the encoder & decoders have been adapted so that they still output the same format, despite the case classes having changed), so there's currently no need to introduce a new fallback 'legacy' decoder (several of which were recently deleted with #3785). This also means the JSON metadata added to PR descriptions (originally introduced in #3466) also remains the same for now, though I think it will be reasonable to adjust the format for `Update.ForArtifactId` to something more logical in future.
@rtyley rtyley force-pushed the replace-update-ForArtifactId-NewerVersions-with-ArtifactUpdateCandidates branch from 28c68e6 to df28ac4 Compare January 18, 2026 09:37
rtyley added a commit that referenced this pull request Jan 18, 2026
Add deserialisation test for VersionsCache Value

Add json fixture for new Value type

Add VersionWithFirstSeen

Add merging of new and old first-seen version data

Add deserialisation test for new format

It looks like we probably want to update UpdatesConfig.scala to add a
new method `isTooNew: Update.ForArtifactId => FilterResult` which rules
out proposed updates that use too new a version (according to the user’s
config).

To do that, we reckon we’ll need Update.ForArtifactId to list versions
with their firstSeen information, i.e. update the contained type from
Version to VersionWithFirstSeen. This commit begins making that
change (and so will not compile).

Replace withNewerVersions with supersedes

This commit removes withNewerVersions and replaces the one usage of it
with a new function supersedes, which checks whether an update matches
another while having a newer nextVersion.

withNewerVersions was a little awkward to update to work with us having
changed some Versions to VersionWithFirstSeens, because it accepts a
list of Versions (rather than just producing one).

It was originally added in response to [this PR
comment](#1667 (comment)),
but we reckon replacing it with a slightly different check of the
group and artifact IDs makes for clearer, more explicit code. We
think (though it’s somewhat hard to tell) that this doesn’t remove any
existing functionality: part of the argument for adding
withNewerVersions in the first place was to support Update.Group, but it
appears to only be called for Update.Singles.

Add CooldownConfig

To facilitate using dependency cooldowns, this commit adds a class
CooldownConfig to represent a user’s desired cooldown settings: a list
of ages (specified as finite durations, using the same string syntax as
supported for pullRequests.frequency), each with a list of
UpdatePatterns for the age to apply to.

Co-authored-by: Roberto Tyley <roberto.tyley@guardian.co.uk>
rtyley added a commit that referenced this pull request Jan 18, 2026
Add deserialisation test for VersionsCache Value

Add json fixture for new Value type

Add VersionWithFirstSeen

Add merging of new and old first-seen version data

Add deserialisation test for new format

It looks like we probably want to update UpdatesConfig.scala to add a
new method `isTooNew: Update.ForArtifactId => FilterResult` which rules
out proposed updates that use too new a version (according to the user’s
config).

To do that, we reckon we’ll need Update.ForArtifactId to list versions
with their firstSeen information, i.e. update the contained type from
Version to VersionWithFirstSeen. This commit begins making that
change (and so will not compile).

Replace withNewerVersions with supersedes

This commit removes withNewerVersions and replaces the one usage of it
with a new function supersedes, which checks whether an update matches
another while having a newer nextVersion.

withNewerVersions was a little awkward to update to work with us having
changed some Versions to VersionWithFirstSeens, because it accepts a
list of Versions (rather than just producing one).

It was originally added in response to [this PR
comment](#1667 (comment)),
but we reckon replacing it with a slightly different check of the
group and artifact IDs makes for clearer, more explicit code. We
think (though it’s somewhat hard to tell) that this doesn’t remove any
existing functionality: part of the argument for adding
withNewerVersions in the first place was to support Update.Group, but it
appears to only be called for Update.Singles.

Add CooldownConfig

To facilitate using dependency cooldowns, this commit adds a class
CooldownConfig to represent a user’s desired cooldown settings: a list
of ages (specified as finite durations, using the same string syntax as
supported for pullRequests.frequency), each with a list of
UpdatePatterns for the age to apply to.

Co-authored-by: Roberto Tyley <roberto.tyley@guardian.co.uk>
rtyley added a commit that referenced this pull request Jan 18, 2026
Add deserialisation test for VersionsCache Value

Add json fixture for new Value type

Add VersionWithFirstSeen

Add merging of new and old first-seen version data

Add deserialisation test for new format

It looks like we probably want to update UpdatesConfig.scala to add a
new method `isTooNew: Update.ForArtifactId => FilterResult` which rules
out proposed updates that use too new a version (according to the user’s
config).

To do that, we reckon we’ll need Update.ForArtifactId to list versions
with their firstSeen information, i.e. update the contained type from
Version to VersionWithFirstSeen. This commit begins making that
change (and so will not compile).

Replace withNewerVersions with supersedes

This commit removes withNewerVersions and replaces the one usage of it
with a new function supersedes, which checks whether an update matches
another while having a newer nextVersion.

withNewerVersions was a little awkward to update to work with us having
changed some Versions to VersionWithFirstSeens, because it accepts a
list of Versions (rather than just producing one).

It was originally added in response to [this PR
comment](#1667 (comment)),
but we reckon replacing it with a slightly different check of the
group and artifact IDs makes for clearer, more explicit code. We
think (though it’s somewhat hard to tell) that this doesn’t remove any
existing functionality: part of the argument for adding
withNewerVersions in the first place was to support Update.Group, but it
appears to only be called for Update.Singles.

Add CooldownConfig

To facilitate using dependency cooldowns, this commit adds a class
CooldownConfig to represent a user’s desired cooldown settings: a list
of ages (specified as finite durations, using the same string syntax as
supported for pullRequests.frequency), each with a list of
UpdatePatterns for the age to apply to.

Co-authored-by: Roberto Tyley <roberto.tyley@guardian.co.uk>
@rtyley
Copy link
Contributor Author

rtyley commented Jan 19, 2026

I like these more specific classes.
Everyone will benefit from easier code understanding

Many thanks @mzuehlke !

@rtyley rtyley merged commit 63c5d11 into main Jan 19, 2026
18 checks passed
rtyley pushed a commit that referenced this pull request Jan 20, 2026
Add deserialisation test for VersionsCache Value

Add json fixture for new Value type

Add VersionWithFirstSeen

Add merging of new and old first-seen version data

Add deserialisation test for new format

It looks like we probably want to update UpdatesConfig.scala to add a
new method `isTooNew: Update.ForArtifactId => FilterResult` which rules
out proposed updates that use too new a version (according to the user’s
config).

To do that, we reckon we’ll need Update.ForArtifactId to list versions
with their firstSeen information, i.e. update the contained type from
Version to VersionWithFirstSeen. This commit begins making that
change (and so will not compile).

Replace withNewerVersions with supersedes

This commit removes withNewerVersions and replaces the one usage of it
with a new function supersedes, which checks whether an update matches
another while having a newer nextVersion.

withNewerVersions was a little awkward to update to work with us having
changed some Versions to VersionWithFirstSeens, because it accepts a
list of Versions (rather than just producing one).

It was originally added in response to [this PR
comment](#1667 (comment)),
but we reckon replacing it with a slightly different check of the
group and artifact IDs makes for clearer, more explicit code. We
think (though it’s somewhat hard to tell) that this doesn’t remove any
existing functionality: part of the argument for adding
withNewerVersions in the first place was to support Update.Group, but it
appears to only be called for Update.Singles.

Add CooldownConfig

To facilitate using dependency cooldowns, this commit adds a class
CooldownConfig to represent a user’s desired cooldown settings: a list
of ages (specified as finite durations, using the same string syntax as
supported for pullRequests.frequency), each with a list of
UpdatePatterns for the age to apply to.

Co-authored-by: Roberto Tyley <roberto.tyley@guardian.co.uk>

# Conflicts:
#	modules/core/src/main/scala/org/scalasteward/core/data/Update.scala
rtyley pushed a commit that referenced this pull request Jan 20, 2026
Add deserialisation test for VersionsCache Value

Add json fixture for new Value type

Add VersionWithFirstSeen

Add merging of new and old first-seen version data

Add deserialisation test for new format

It looks like we probably want to update UpdatesConfig.scala to add a
new method `isTooNew: Update.ForArtifactId => FilterResult` which rules
out proposed updates that use too new a version (according to the user’s
config).

To do that, we reckon we’ll need Update.ForArtifactId to list versions
with their firstSeen information, i.e. update the contained type from
Version to VersionWithFirstSeen. This commit begins making that
change (and so will not compile).

Replace withNewerVersions with supersedes

This commit removes withNewerVersions and replaces the one usage of it
with a new function supersedes, which checks whether an update matches
another while having a newer nextVersion.

withNewerVersions was a little awkward to update to work with us having
changed some Versions to VersionWithFirstSeens, because it accepts a
list of Versions (rather than just producing one).

It was originally added in response to [this PR
comment](#1667 (comment)),
but we reckon replacing it with a slightly different check of the
group and artifact IDs makes for clearer, more explicit code. We
think (though it’s somewhat hard to tell) that this doesn’t remove any
existing functionality: part of the argument for adding
withNewerVersions in the first place was to support Update.Group, but it
appears to only be called for Update.Singles.

Add CooldownConfig

To facilitate using dependency cooldowns, this commit adds a class
CooldownConfig to represent a user’s desired cooldown settings: a list
of ages (specified as finite durations, using the same string syntax as
supported for pullRequests.frequency), each with a list of
UpdatePatterns for the age to apply to.

Co-authored-by: Roberto Tyley <roberto.tyley@guardian.co.uk>

# Conflicts:
#	modules/core/src/main/scala/org/scalasteward/core/data/Update.scala
@mzuehlke mzuehlke added this to the 0.37.1 milestone Jan 23, 2026
rtyley pushed a commit to guardian/scala-steward that referenced this pull request Jan 23, 2026
Add deserialisation test for VersionsCache Value

Add json fixture for new Value type

Add VersionWithFirstSeen

Add merging of new and old first-seen version data

Add deserialisation test for new format

It looks like we probably want to update UpdatesConfig.scala to add a
new method `isTooNew: Update.ForArtifactId => FilterResult` which rules
out proposed updates that use too new a version (according to the user’s
config).

To do that, we reckon we’ll need Update.ForArtifactId to list versions
with their firstSeen information, i.e. update the contained type from
Version to VersionWithFirstSeen. This commit begins making that
change (and so will not compile).

Replace withNewerVersions with supersedes

This commit removes withNewerVersions and replaces the one usage of it
with a new function supersedes, which checks whether an update matches
another while having a newer nextVersion.

withNewerVersions was a little awkward to update to work with us having
changed some Versions to VersionWithFirstSeens, because it accepts a
list of Versions (rather than just producing one).

It was originally added in response to [this PR
comment](scala-steward-org#1667 (comment)),
but we reckon replacing it with a slightly different check of the
group and artifact IDs makes for clearer, more explicit code. We
think (though it’s somewhat hard to tell) that this doesn’t remove any
existing functionality: part of the argument for adding
withNewerVersions in the first place was to support Update.Group, but it
appears to only be called for Update.Singles.

Add CooldownConfig

To facilitate using dependency cooldowns, this commit adds a class
CooldownConfig to represent a user’s desired cooldown settings: a list
of ages (specified as finite durations, using the same string syntax as
supported for pullRequests.frequency), each with a list of
UpdatePatterns for the age to apply to.

Co-authored-by: Roberto Tyley <roberto.tyley@guardian.co.uk>

# Conflicts:
#	modules/core/src/main/scala/org/scalasteward/core/data/Update.scala

# Conflicts:
#	modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala
rtyley pushed a commit to guardian/scala-steward that referenced this pull request Jan 29, 2026
Add deserialisation test for VersionsCache Value

Add json fixture for new Value type

Add VersionWithFirstSeen

Add merging of new and old first-seen version data

Add deserialisation test for new format

It looks like we probably want to update UpdatesConfig.scala to add a
new method `isTooNew: Update.ForArtifactId => FilterResult` which rules
out proposed updates that use too new a version (according to the user’s
config).

To do that, we reckon we’ll need Update.ForArtifactId to list versions
with their firstSeen information, i.e. update the contained type from
Version to VersionWithFirstSeen. This commit begins making that
change (and so will not compile).

Replace withNewerVersions with supersedes

This commit removes withNewerVersions and replaces the one usage of it
with a new function supersedes, which checks whether an update matches
another while having a newer nextVersion.

withNewerVersions was a little awkward to update to work with us having
changed some Versions to VersionWithFirstSeens, because it accepts a
list of Versions (rather than just producing one).

It was originally added in response to [this PR
comment](scala-steward-org#1667 (comment)),
but we reckon replacing it with a slightly different check of the
group and artifact IDs makes for clearer, more explicit code. We
think (though it’s somewhat hard to tell) that this doesn’t remove any
existing functionality: part of the argument for adding
withNewerVersions in the first place was to support Update.Group, but it
appears to only be called for Update.Singles.

Add CooldownConfig

To facilitate using dependency cooldowns, this commit adds a class
CooldownConfig to represent a user’s desired cooldown settings: a list
of ages (specified as finite durations, using the same string syntax as
supported for pullRequests.frequency), each with a list of
UpdatePatterns for the age to apply to.

Co-authored-by: Roberto Tyley <roberto.tyley@guardian.co.uk>

# Conflicts:
#	modules/core/src/main/scala/org/scalasteward/core/data/Update.scala

# Conflicts:
#	modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala
rtyley pushed a commit to guardian/scala-steward that referenced this pull request Jan 29, 2026
Add deserialisation test for VersionsCache Value

Add json fixture for new Value type

Add VersionWithFirstSeen

Add merging of new and old first-seen version data

Add deserialisation test for new format

It looks like we probably want to update UpdatesConfig.scala to add a
new method `isTooNew: Update.ForArtifactId => FilterResult` which rules
out proposed updates that use too new a version (according to the user’s
config).

To do that, we reckon we’ll need Update.ForArtifactId to list versions
with their firstSeen information, i.e. update the contained type from
Version to VersionWithFirstSeen. This commit begins making that
change (and so will not compile).

Replace withNewerVersions with supersedes

This commit removes withNewerVersions and replaces the one usage of it
with a new function supersedes, which checks whether an update matches
another while having a newer nextVersion.

withNewerVersions was a little awkward to update to work with us having
changed some Versions to VersionWithFirstSeens, because it accepts a
list of Versions (rather than just producing one).

It was originally added in response to [this PR
comment](scala-steward-org#1667 (comment)),
but we reckon replacing it with a slightly different check of the
group and artifact IDs makes for clearer, more explicit code. We
think (though it’s somewhat hard to tell) that this doesn’t remove any
existing functionality: part of the argument for adding
withNewerVersions in the first place was to support Update.Group, but it
appears to only be called for Update.Singles.

Add CooldownConfig

To facilitate using dependency cooldowns, this commit adds a class
CooldownConfig to represent a user’s desired cooldown settings: a list
of ages (specified as finite durations, using the same string syntax as
supported for pullRequests.frequency), each with a list of
UpdatePatterns for the age to apply to.

Co-authored-by: Roberto Tyley <roberto.tyley@guardian.co.uk>

# Conflicts:
#	modules/core/src/main/scala/org/scalasteward/core/data/Update.scala

# Conflicts:
#	modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PullRequestRepository unnecessarily persists newerVersions - ie the data about what possible versions might have been eligible

2 participants