Skip to content

feat(bootstrap): add --max-release-age for multiple-versions mode#1089

Merged
LalatenduMohanty merged 1 commit intopython-wheel-build:mainfrom
rd4398:date-range-filtering
Apr 28, 2026
Merged

feat(bootstrap): add --max-release-age for multiple-versions mode#1089
LalatenduMohanty merged 1 commit intopython-wheel-build:mainfrom
rd4398:date-range-filtering

Conversation

@rd4398
Copy link
Copy Markdown
Contributor

@rd4398 rd4398 commented Apr 23, 2026

Add --max-release-age flag to bootstrap command to filter out package versions older than N days. This flag is required when --multiple-versions is enabled, preventing builds of hundreds of old releases. Together with --min-release-age (cooldown), it creates a sliding time window for version selection in the multiple-versions bootstrap feature.

Closes #1037

@rd4398 rd4398 requested a review from a team as a code owner April 23, 2026 18:03
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@rd4398 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 59 minutes and 23 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: edb19bbc-c8b2-4a31-9817-f034da09e030

📥 Commits

Reviewing files that changed from the base of the PR and between 7821ded and f280d30.

📒 Files selected for processing (10)
  • e2e/ci_bootstrap_suite.sh
  • e2e/test_bootstrap_max_release_age.sh
  • e2e/test_bootstrap_multiple_versions.sh
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/context.py
  • src/fromager/resolver.py
  • src/fromager/sources.py
  • tests/test_bootstrap.py
  • tests/test_cooldown.py
📝 Walkthrough

Walkthrough

Adds date-based release-age filtering to bootstrap resolution. Introduces a --max-release-age CLI option for bootstrap commands, stores it on WorkContext (as a timedelta), and computes a max-age cutoff (bootstrap time or now) in the resolver. When provided, candidate versions from source providers are filtered out if their upload_time is older than the cutoff; candidates with no upload_time are retained. If filtering removes all candidates, the resolver raises an exception. Tests (unit and e2e) and CI scripts are updated/added to validate the option and filtering behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature addition: a --max-release-age flag for the bootstrap command's multiple-versions mode.
Description check ✅ Passed The description accurately explains the purpose, behavior, and relationship of the --max-release-age flag with --min-release-age, and references the related issue.
Linked Issues check ✅ Passed The PR implements date range filtering [#1037] by adding --max-release-age to filter versions older than N days, paired with --min-release-age to create a sliding time window for version selection.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing --max-release-age filtering [#1037]: CLI option addition, context/resolver updates, filtering logic, and comprehensive tests. No unrelated modifications present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mergify mergify Bot added the ci label Apr 23, 2026
@rd4398
Copy link
Copy Markdown
Contributor Author

rd4398 commented Apr 23, 2026

This adds --max-release-age to the bootstrap (and bootstrap-parallel) commands as part of the multiple-versions bootstrap feature. It complements the existing --min-release-age (cooldown from PR #1018) to create a sliding time window for version selection.

What it does:

  • --max-release-age N rejects package versions published more than N days ago
  • Required when --multiple-versions is enabled; ignored otherwise
  • Filtering happens in find_all_matching_from_provider() on the resolved candidates list — candidates without upload_time (e.g. git URLs) pass through
  • If all candidates are older than the cutoff, all are kept with a warning to avoid empty resolution
  • Uses the cooldown's bootstrap_time when available for consistent cutoffs across a single run

@rd4398 rd4398 force-pushed the date-range-filtering branch from 8f748f3 to 58b9c20 Compare April 23, 2026 18:06
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
tests/test_bootstrap.py (1)

744-745: Prefer behavior assertions over log-string matching.

assert tmp_context.max_release_age == timedelta(days=45) already proves the CLI wired through correctly. The log-substring check is brittle if the message wording changes — consider dropping it.

     assert result.exit_code == 0
     assert tmp_context.max_release_age == timedelta(days=45)
-    assert "rejecting versions older than 45 days" in caplog.text

Based on learnings: avoid asserting exact log output strings since they are brittle implementation details.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_bootstrap.py` around lines 744 - 745, Remove the brittle
log-string assertion and rely on the behavior assertion already present: keep
the existing assert tmp_context.max_release_age == timedelta(days=45) and delete
the assert "rejecting versions older than 45 days" in caplog.text; if you still
want to validate logging behavior, replace the exact substring check with a
non-brittle assertion that a log record at the expected level was emitted (using
caplog.records or any log-level check) rather than matching message text,
referencing tmp_context.max_release_age and caplog for locating the test logic.
src/fromager/bootstrap_requirement_resolver.py (1)

176-179: Cross-module use of a private helper.

resolver._compute_max_age_cutoff is used here and in sources.py. Since it has real callers outside resolver.py, consider dropping the leading underscore (or wrapping it in find_all_matching_from_provider so callers don't need to compute the cutoff themselves).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/bootstrap_requirement_resolver.py` around lines 176 - 179, The
call site uses the internal helper resolver._compute_max_age_cutoff from outside
resolver.py; either make the helper public by renaming it to
compute_max_age_cutoff, or encapsulate the cutoff computation inside
resolver.find_all_matching_from_provider so callers (like the code calling
resolver.find_all_matching_from_provider and other callers such as sources.py)
no longer need to call a private method. Update references to
resolver._compute_max_age_cutoff accordingly (rename imports/usages to
resolver.compute_max_age_cutoff if you choose the public rename) or change
find_all_matching_from_provider signature/implementation to accept ctx and
compute the max_age_cutoff internally and remove external calls to the private
helper.
src/fromager/commands/bootstrap.py (1)

113-119: Consider default=None instead of the 0 sentinel.

Using 0 to mean "disabled" while also being a legal IntRange(min=0) value is what creates the ambiguity above. Making the option int | None with default=None lets you distinguish "not provided" from "provided as 0" cleanly, and aligns with how cooldown is modelled on WorkContext.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/commands/bootstrap.py` around lines 113 - 119, Change the
"--max-release-age" option to use None as the disabled sentinel by setting
default=None and allowing an optional int type (so the option becomes int | None
rather than always 0..). Update the option definition for "max_release_age" to
accept None (preserve the min=0 constraint when a value is provided) and adjust
any downstream checks that currently treat 0 as "disabled" (search for usages of
max_release_age) to treat None as "disabled" instead; also revise the help text
to state that omitting the option disables the check and ensure the existing
requirement logic for "--multiple-versions" still enforces that a value must be
provided when required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/test_bootstrap_max_release_age.sh`:
- Around line 44-55: The test is non-deterministic because the current
requirement range 'certifi>=2025.11,<=2026.5' allows newer releases that can
slip past the "within window" assertions; update the hardcoded versions iterated
by the for loop (the `version` list checked by find
"$OUTDIR/wheels-repo/downloads/" for 'certifi-*.whl') and tighten the upper
bound in the requirement string to a fixed cutoff (e.g., change '<=2026.5' to
'<=2026.4.22') so the test consistently expects specific certifi wheels; also
ensure any related wheel checks (like the setuptools check referenced around the
later find pattern) use the same tightened upper bound to keep expectations
deterministic.

In `@src/fromager/commands/bootstrap.py`:
- Around line 166-170: The current check treats 0 as "disabled" but raises a
misleading message when users pass --max-release-age 0 with --multiple-versions;
update the validation around multiple_versions/max_release_age so it requires a
positive value and gives a clear message (e.g. raise
click.UsageError("--max-release-age must be > 0 when using
--multiple-versions")) or alternatively change the CLI option for
max_release_age to use click.IntRange(min=1) so the CLI enforces a positive
integer; locate the validation that references multiple_versions and
max_release_age in bootstrap.py and apply one of these fixes.

In `@src/fromager/resolver.py`:
- Around line 276-281: The max-age filter currently keeps candidates with
upload_time None which diverges from is_blocked_by_cooldown's behavior for
providers that declare supports_upload_time=True; update the max-age filtering
logic (the block using max_age_cutoff and candidates_list) to only treat missing
upload_time as acceptable when the candidate.provider.supports_upload_time is
False (i.e., if provider.supports_upload_time is True, treat upload_time None as
failing the max-age check), or alternatively add a clear docstring comment on
the max-age filter explaining the intentional asymmetry; reference
max_age_cutoff, candidates_list, upload_time, and is_blocked_by_cooldown when
making the change so the behavior is aligned or explicitly documented.
- Around line 292-298: The filter that checks release age currently falls back
to keeping all candidates and only logs a warning, which defeats the
--max-release-age filter; in the resolver.py function that filters candidates by
max release age (the block referencing req.name and candidates_list) change the
behavior to fail loudly: instead of returning all candidates when every
candidate is older than the cutoff, raise a clear exception (e.g., a ValueError
or a custom ResolutionError) so callers can decide how to proceed; additionally,
update the function docstring to state the explicit "fail on cutoff" policy and
mention the raised exception type so callers know to catch it.

---

Nitpick comments:
In `@src/fromager/bootstrap_requirement_resolver.py`:
- Around line 176-179: The call site uses the internal helper
resolver._compute_max_age_cutoff from outside resolver.py; either make the
helper public by renaming it to compute_max_age_cutoff, or encapsulate the
cutoff computation inside resolver.find_all_matching_from_provider so callers
(like the code calling resolver.find_all_matching_from_provider and other
callers such as sources.py) no longer need to call a private method. Update
references to resolver._compute_max_age_cutoff accordingly (rename
imports/usages to resolver.compute_max_age_cutoff if you choose the public
rename) or change find_all_matching_from_provider signature/implementation to
accept ctx and compute the max_age_cutoff internally and remove external calls
to the private helper.

In `@src/fromager/commands/bootstrap.py`:
- Around line 113-119: Change the "--max-release-age" option to use None as the
disabled sentinel by setting default=None and allowing an optional int type (so
the option becomes int | None rather than always 0..). Update the option
definition for "max_release_age" to accept None (preserve the min=0 constraint
when a value is provided) and adjust any downstream checks that currently treat
0 as "disabled" (search for usages of max_release_age) to treat None as
"disabled" instead; also revise the help text to state that omitting the option
disables the check and ensure the existing requirement logic for
"--multiple-versions" still enforces that a value must be provided when
required.

In `@tests/test_bootstrap.py`:
- Around line 744-745: Remove the brittle log-string assertion and rely on the
behavior assertion already present: keep the existing assert
tmp_context.max_release_age == timedelta(days=45) and delete the assert
"rejecting versions older than 45 days" in caplog.text; if you still want to
validate logging behavior, replace the exact substring check with a non-brittle
assertion that a log record at the expected level was emitted (using
caplog.records or any log-level check) rather than matching message text,
referencing tmp_context.max_release_age and caplog for locating the test logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 048507fd-9138-44b5-96b3-46d791f117f7

📥 Commits

Reviewing files that changed from the base of the PR and between 05c65ab and bf791be.

📒 Files selected for processing (9)
  • e2e/test_bootstrap_max_release_age.sh
  • e2e/test_bootstrap_multiple_versions.sh
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/context.py
  • src/fromager/resolver.py
  • src/fromager/sources.py
  • tests/test_bootstrap.py
  • tests/test_cooldown.py

Comment thread e2e/test_bootstrap_max_release_age.sh
Comment thread src/fromager/commands/bootstrap.py Outdated
Comment thread src/fromager/resolver.py
Comment thread src/fromager/resolver.py
EmilienM
EmilienM previously approved these changes Apr 23, 2026
Comment thread src/fromager/commands/bootstrap.py Outdated
Comment thread src/fromager/commands/bootstrap.py Outdated
Comment thread src/fromager/resolver.py
@rd4398 rd4398 force-pushed the date-range-filtering branch 2 times, most recently from 63ed4fa to e0fa9e5 Compare April 23, 2026 19:45
Comment thread src/fromager/commands/bootstrap.py Outdated
Comment thread src/fromager/commands/bootstrap.py Outdated
@rd4398 rd4398 force-pushed the date-range-filtering branch from 42c83fb to 43ac347 Compare April 27, 2026 17:21
Comment thread src/fromager/context.py
Comment thread src/fromager/resolver.py
Comment thread src/fromager/resolver.py Outdated
@rd4398 rd4398 force-pushed the date-range-filtering branch from 5b048bd to ebd42b9 Compare April 27, 2026 19:00
@LalatenduMohanty
Copy link
Copy Markdown
Member

@mergify rebase

@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented Apr 27, 2026

Deprecation notice: This pull request comes from a fork and was rebased using bot_account impersonation. This capability will be removed on July 1, 2026. After this date, the rebase action will no longer be able to rebase fork pull requests with this configuration. Please switch to the update action/command to ensure compatibility going forward.

@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented Apr 27, 2026

rebase

✅ Branch has been successfully rebased

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
tests/test_cooldown.py (1)

714-716: Avoid log-text assertions in these unit tests

These checks couple tests to log wording instead of behavior. The version-set assertions already validate the filtering logic.

Proposed simplification
-    assert "found 3 candidate(s)" in caplog.text
-    assert "have 2 candidate(s)" in caplog.text
-    assert "published within" in caplog.text
@@
-    assert "found 3 candidate(s)" in caplog.text
-    assert "have 1 candidate(s)" in caplog.text
-    assert "published within" in caplog.text

Based on learnings: In this project’s Python test suite, avoid asserting on exact log output strings since they are brittle implementation details.

Also applies to: 738-740

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cooldown.py` around lines 714 - 716, Remove the brittle log-text
assertions that inspect caplog.text (the three asserts containing "found 3
candidate(s)", "have 2 candidate(s)", and "published within" and the similar
trio at the later block) and rely on the existing version-set/assertions that
validate filtering behavior; specifically, delete or comment out those
caplog-based assertions in tests/test_cooldown.py so the test only asserts on
functional outputs (e.g., the version/filter result variables) instead of exact
log wording.
tests/test_bootstrap.py (1)

745-745: Drop the exact log-string assertion from this CLI test

tmp_context.max_release_age == timedelta(days=45) already verifies the behavior. The log text check adds brittleness without increasing confidence.

Proposed change
-    assert "rejecting versions older than 45 days" in caplog.text

Based on learnings: In this project’s Python test suite, avoid asserting on exact log output strings since they are brittle implementation details.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_bootstrap.py` at line 745, Remove the brittle exact log-string
assertion in the CLI test: delete the line asserting "rejecting versions older
than 45 days" in tests/test_bootstrap.py and rely on the existing assertion that
tmp_context.max_release_age == timedelta(days=45) to verify behavior; ensure no
other tests depend on caplog.text here and keep the tmp_context.max_release_age
check as the authoritative verification.
e2e/test_bootstrap_max_release_age.sh (1)

76-84: Prefer functional E2E checks over log-content checks

The wheel presence/absence assertions already prove filtering behavior. Grepping log phrasing is brittle and can fail on harmless wording changes.

Proposed change
-# Verify that max-release-age filtering was applied (check log)
-echo ""
-echo "Checking log for max-release-age filtering..."
-if grep -q "published within.*days" "$OUTDIR/bootstrap.log"; then
-  echo "✓ Log confirms max-release-age filtering was applied"
-else
-  echo "✗ No max-release-age filtering found in log"
-  exit 1
-fi

Based on learnings: In fromager tests, prefer verifying functional behavior over asserting log output strings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/test_bootstrap_max_release_age.sh` around lines 76 - 84, The test
currently greps bootstrap.log for the phrase "published within.*days" (the block
using grep -q on "$OUTDIR/bootstrap.log") which is brittle; remove that
log-content check and instead assert the filtering via the existing functional
checks (the wheel presence/absence assertions already in this script), i.e.
delete the if/then/else grep block and replace it with a short comment stating
that filtering is validated by the wheel artifact assertions, referencing the
OUTDIR/bootstrap.log and the wheel existence checks in this script so
maintainers know where the functional assertions live.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_cooldown.py`:
- Around line 762-779: The test test_max_release_age_all_too_old_raises asserts
a ResolverException when all candidates exceed max_age_cutoff, but the PR
changes resolvers.find_all_matching_from_provider behavior to retain all
candidates and emit a warning instead of raising; update the test to call
resolver.find_all_matching_from_provider with provider =
resolver.PyPIProvider(include_sdists=True) and Requirement("test-pkg"), assert
it returns the expected candidate list (e.g., length 3 or contains the versions
from _cooldown_json_response) and assert a warning was emitted (use pytest.warns
or caplog) about "max-release-age cutoff" rather than expecting
resolvelib.resolvers.ResolverException. Ensure you reference the test function
name test_max_release_age_all_too_old_raises and the function
find_all_matching_from_provider when making the change.

---

Nitpick comments:
In `@e2e/test_bootstrap_max_release_age.sh`:
- Around line 76-84: The test currently greps bootstrap.log for the phrase
"published within.*days" (the block using grep -q on "$OUTDIR/bootstrap.log")
which is brittle; remove that log-content check and instead assert the filtering
via the existing functional checks (the wheel presence/absence assertions
already in this script), i.e. delete the if/then/else grep block and replace it
with a short comment stating that filtering is validated by the wheel artifact
assertions, referencing the OUTDIR/bootstrap.log and the wheel existence checks
in this script so maintainers know where the functional assertions live.

In `@tests/test_bootstrap.py`:
- Line 745: Remove the brittle exact log-string assertion in the CLI test:
delete the line asserting "rejecting versions older than 45 days" in
tests/test_bootstrap.py and rely on the existing assertion that
tmp_context.max_release_age == timedelta(days=45) to verify behavior; ensure no
other tests depend on caplog.text here and keep the tmp_context.max_release_age
check as the authoritative verification.

In `@tests/test_cooldown.py`:
- Around line 714-716: Remove the brittle log-text assertions that inspect
caplog.text (the three asserts containing "found 3 candidate(s)", "have 2
candidate(s)", and "published within" and the similar trio at the later block)
and rely on the existing version-set/assertions that validate filtering
behavior; specifically, delete or comment out those caplog-based assertions in
tests/test_cooldown.py so the test only asserts on functional outputs (e.g., the
version/filter result variables) instead of exact log wording.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8daac40b-9f5f-4555-abd5-de93128e3112

📥 Commits

Reviewing files that changed from the base of the PR and between bf791be and 7821ded.

📒 Files selected for processing (10)
  • e2e/ci_bootstrap_suite.sh
  • e2e/test_bootstrap_max_release_age.sh
  • e2e/test_bootstrap_multiple_versions.sh
  • src/fromager/bootstrap_requirement_resolver.py
  • src/fromager/commands/bootstrap.py
  • src/fromager/context.py
  • src/fromager/resolver.py
  • src/fromager/sources.py
  • tests/test_bootstrap.py
  • tests/test_cooldown.py
✅ Files skipped from review due to trivial changes (2)
  • e2e/ci_bootstrap_suite.sh
  • src/fromager/sources.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • e2e/test_bootstrap_multiple_versions.sh
  • src/fromager/commands/bootstrap.py
  • src/fromager/resolver.py
  • src/fromager/bootstrap_requirement_resolver.py

Comment thread tests/test_cooldown.py Outdated
Add --max-release-age flag to bootstrap command to filter out package
versions older than N days. This flag is required when --multiple-versions
is enabled, preventing builds of hundreds of old releases. Together with
--min-release-age (cooldown), it creates a sliding time window for
version selection in the multiple-versions bootstrap feature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Rohan Devasthale <rdevasth@redhat.com>
@rd4398 rd4398 force-pushed the date-range-filtering branch from 952b43d to f280d30 Compare April 27, 2026 21:28
@LalatenduMohanty LalatenduMohanty merged commit 478701b into python-wheel-build:main Apr 28, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Date Range Filtering

4 participants