Skip to content

Fix retention policy bucket collisions and improve directory pruning - #1

Merged
ardabeyazoglu merged 5 commits into
mainfrom
claude/retention-library-review-l7qag1
Jul 14, 2026
Merged

Fix retention policy bucket collisions and improve directory pruning#1
ardabeyazoglu merged 5 commits into
mainfrom
claude/retention-library-review-l7qag1

Conversation

@ardabeyazoglu

Copy link
Copy Markdown
Owner

Summary

This PR fixes critical bugs in the retention policy algorithm where different dates could collide in the same bucket, and improves the directory pruning logic to handle nested directories and symlinks correctly.

Key Changes

Retention Policy Fixes

  • Fixed bucket collision bug: Period-based policies (daily, weekly, monthly, hourly, yearly) now use fixed-width, zero-padded bucket keys instead of concatenating unpadded integers. This prevents dates like 2024-01-15 and 2024-11-05 from both mapping to "2024115" and being silently deduplicated.
  • Fixed ISO week handling: Weekly policy now uses ISO-8601 year (format('oW')) instead of calendar year, ensuring dates at year boundaries (e.g., 2019-12-30 in ISO week 01 of 2020) are bucketed correctly.
  • Fixed midnight hour exclusion: Removed the $hour > 0 guard that prevented files created at midnight (hour 0) from being retained by the hourly policy while still consuming a bucket slot.
  • Fixed period policy guards: Removed unnecessary $day > 0, $week > 0, $month > 0, and $year > 0 guards that could exclude valid files.
  • Improved safety net logic: The fallback "keep at least 1 file" now correctly handles associative arrays when a groupHandler is used.

Directory Pruning Improvements

  • Fixed nested directory removal: Changed from LEAVES_ONLY to CHILD_FIRST iteration mode, ensuring nested directories are properly removed before their parents.
  • Fixed symlink handling: Symlinks are now safely removed without following them or touching targets outside the pruned tree. The iterator uses FilesystemIterator::SKIP_DOTS and explicitly checks isLink() to handle symlinks correctly.
  • Improved safeguard logic: The risky path check now handles cases where realpath() returns false by falling back to the raw path.

Code Quality

  • Fixed sort comparator: Replaced ternary operator with spaceship operator (<=>) for cleaner, more idiomatic comparison.
  • Updated dependencies: Changed from monolog/monolog to psr/log as the only required dependency, reducing bloat.
  • Updated CI/CD: Migrated from php-actions to shivammathur/setup-php for better control and reliability.

Testing

Comprehensive edge-case tests added covering:

  • Daily policy with dates that previously collided
  • Hourly policy with midnight files
  • Weekly policy at year boundaries and across year transitions
  • Empty policies and negative values
  • Sort stability with equal timestamps
  • Grouping with period policies
  • Nested directory and symlink pruning

https://claude.ai/code/session_01WtQAKxkfQRpobn9ZGSotSz

claude added 5 commits July 14, 2026 16:06
Correctness fixes in the restic-style keep-* policy computation:

- Period bucket keys are now derived from fixed-width date formats in
  FileInfo (Ymd / YmdH / Ym / oW / Y) instead of concatenating unpadded
  integers. This fixes silent collisions where different days/hours mapped
  to the same key (e.g. 2024-01-15 and 2024-11-05 both became "2024115").
- Weekly buckets now use the ISO-8601 year ("o") paired with the ISO week,
  so dates in week 01/53 of a neighbouring year are bucketed correctly
  (e.g. 2019-12-30 is ISO week 01 of 2020, not 2019).
- keep-hourly no longer excludes files created at hour 0 (midnight); the
  `$hour > 0` guard let midnight files consume a bucket slot without ever
  being retained.
- findFiles() sort comparator now returns 0 for equal timestamps instead
  of being non-transitive, giving deterministic ordering.
- The "keep at least one" fallback no longer assumes a numeric key, which
  is invalid once files have been grouped into an associative array.

Pruning robustness / safety:

- Recursive directory deletion iterates CHILD_FIRST so nested directory
  trees (deeper than one level) are fully removed.
- Symlinks are never followed out of the tree; a symlinked entry is
  unlinked (the link only), leaving its external target untouched.
- The shallow-path safeguard falls back to the raw path when realpath()
  returns false, so it cannot be silently bypassed by a vanished path.

Dependencies / docs:

- Replace the monolog/monolog runtime requirement with psr/log (only the
  PSR-3 interfaces are used), and update the README dependency note.
- Bump the phpunit dev dependency to the patched 10.5.62+ line.
- Fix apply() return-type docblock (Result, not array).

Tests: add coverage for the day/hour index collision, midnight hourly,
ISO-week boundaries, weekly counting across a new year, empty/zero/negative
policy config, equal-timestamp sort stability, grouping under a period
policy, nested-directory pruning, and symlink-escape safety.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtQAKxkfQRpobn9ZGSotSz
…HPUnit

CI failed with "This version of PHPUnit requires PHP >= 8.4.1" on PHP 8.3.
The composer install step was fine (locked phpunit/phpunit 10.5.64 installs
and runs cleanly on PHP 8.3). The phpunit test step's `version` input was
left unset, and php-actions/phpunit@v3 defaults that to "latest" rather
than "composer" as its docs suggest, so it downloaded PHPUnit 13.2.4
instead of using our own vendor/bin/phpunit. Pin version: composer so the
action uses the project's own PHPUnit install.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtQAKxkfQRpobn9ZGSotSz
The version: composer input doesn't run vendor/bin/phpunit directly -- it
reads the version string from composer.lock/composer.json and downloads a
matching PHAR from phar.phpunit.de. That patch version (10.5.64) has no
published PHAR, and the action's curl call doesn't check the HTTP status,
so it silently saved a 404/empty response as phpunit.phar and tried to
execute it ("syntax error: unexpected newline").

vendored_phpunit_path skips version detection and PHAR download entirely,
running the given path directly, so point it at our own composer-installed
vendor/bin/phpunit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtQAKxkfQRpobn9ZGSotSz
vendored_phpunit_path correctly points at vendor/bin/phpunit now, but the
phpunit step's container failed with "Operation not permitted" trying to
chmod +x it. php-actions/composer and php-actions/phpunit each run in their
own Docker container mounting the workspace, and vendor/ ends up owned by
a UID the second container can't modify. Reset permissions on the runner
(outside any container) between the two steps so either container can
read/write/execute it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtQAKxkfQRpobn9ZGSotSz
vendored_phpunit_path correctly pointed at our own vendor/bin/phpunit, but
hit a new failure: "chmod: ... Operation not permitted". php-actions/composer
and php-actions/phpunit each run in separate Docker containers mounting the
workspace, and the phpunit container's user lacks ownership/CAP_FOWNER over
files the composer container wrote -- chmod requires matching ownership or
that capability, which permission bits alone can't grant, so a prior
"chmod -R a+rwX vendor" step didn't help.

This is the third distinct php-actions/* bug hit in a row (wrong default
version, broken PHAR download for our patch version, now this). Switch to
shivammathur/setup-php, the standard action for PHP CI: it installs PHP 8.3
with xdebug directly on the runner, so composer install and vendor/bin/phpunit
run as plain steps under the same user throughout, with no nested-container
ownership mismatch to work around.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtQAKxkfQRpobn9ZGSotSz
@ardabeyazoglu
ardabeyazoglu merged commit f33ce41 into main Jul 14, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants