Skip to content

Add ZonedClock, and remove now() methods and DefaultClock - #133

Open
joelbutcher wants to merge 9 commits into
brick:mainfrom
joelbutcher:zoned-clock-now
Open

joelbutcher wants to merge 9 commits into
brick:mainfrom
joelbutcher:zoned-clock-now

Conversation

@joelbutcher

@joelbutcher joelbutcher commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Supersedes #132, following the direction agreed in that thread and in the review below.

Summary

Clock now only provides the current Instant, like Java 17's InstantSource and Noda Time's IClock. A new ZonedClock wraps a Clock and a TimeZone, like Noda Time's ZonedClock, and is the single way to get the current date or time:

$clock = (new SystemClock())->atTimeZone(TimeZone::parse('Europe/Paris'));

$clock->getInstant();       // Instant
$clock->getZonedDateTime(); // ZonedDateTime in Paris
$clock->getLocalDate();     // LocalDate in Paris
$clock->getLocalDate()->getYearMonth(); // other values are one hop away

$clock->atTimeZone(TimeZone::parse('America/New_York'))->getLocalTime(); // another zone

Dependencies now go one way: clocks depend on value objects, and value objects never depend on clocks.

  • Clock::getTime() is renamed to getInstant(), and Clock gains atTimeZone(TimeZone): ZonedClock. It's named like Instant::atTimeZone() and LocalDateTime::atTimeZone(), which also return a different type.
  • ZonedClock is a final readonly class. It implements Clock, and has:
    • getClock() and getTimeZone()
    • atTimeZone(), which keeps the same wrapped clock
    • getZonedDateTime(), getLocalDateTime(), getLocalDate() and getLocalTime()
  • All now() methods are removed, from DayOfWeek, Instant, LocalDate, LocalDateTime, LocalTime, MonthDay, Quarter, Year, YearMonth, YearWeek and ZonedDateTime.
  • isFuture() and isPast() are removed from Instant, LocalDateTime and ZonedDateTime.
  • DefaultClock is removed, and Stopwatch now requires a Clock.
  • SystemClock stays a plain Clock, so the system time zone is never used implicitly.

Why

  • The time zone stays required. A ZonedClock can't exist without one, so there's no implicit default: not UTC, and not php.ini. This keeps the explicit time zones valued in LocalDate::utc() method #111 and LocalDate::now($timezone /*= system TimeZone*/) #113.
  • The clock is always explicit. No global state: code that reads the current date or time is testable by default, and a test can't leak a frozen clock into the next one.
  • Apps with one time zone and apps with many are both covered:
    • Single-zone apps inject a ZonedClock configured once.
    • Multi-zone apps inject a Clock and bind it per call with atTimeZone().
  • getTime() was ambiguous. It meant Instant on Clock but LocalTime on ZonedDateTime. getInstant() and the ZonedClock getters name their return types.

Breaking changes

This is fine before 1.0. The CHANGELOG lists a migration for each method:

Before After
Clock::getTime() Clock::getInstant()
LocalDate::now($timeZone, $clock) $clock->atTimeZone($timeZone)->getLocalDate()
YearMonth::now(...), YearWeek::now(...), MonthDay::now(...), Quarter::now(...), DayOfWeek::now(...) $zonedClock->getLocalDate()->getYearMonth() etc.
Year::now(...) Year::of($zonedClock->getLocalDate()->getYear())
Instant::now($clock) $clock->getInstant()
$instant->isFuture($clock) $instant->isAfter($clock->getInstant())
DefaultClock::freeze($instant) new FixedClock($instant)
DefaultClock::travelTo($instant) new OffsetClock($clock, Duration::between($clock->getInstant(), $instant))
DefaultClock::travelBy($duration) new OffsetClock($clock, $duration)
DefaultClock::scale($n) new ScaleClock($clock, $n)
new Stopwatch() new Stopwatch($clock)
$fixedClock->setTime($instant) $fixedClock->setInstant($instant)

Custom Clock implementations must rename getTime() to getInstant() and implement atTimeZone().

Related

  • Add LocalDate::getMonthDay() #134 (merged, released in 0.10.1) added LocalDate::getMonthDay(), which makes MonthDay reachable as $zonedClock->getLocalDate()->getMonthDay().

Follow-ups

  • PSR-20: implement Psr\Clock\ClockInterface on ZonedClock, or provide an adapter (both directions).
  • A readable replacement for isPast() / isFuture() that doesn't add methods to the Clock interface (see the discussion below).
  • Possibly an explicit TimeZone::fromSystem().

Tests

  • New ZonedClockTest, with expected values checked against date(1). It includes Tokyo and Los Angeles falling on different dates, and the Europe/London DST change on 2026-03-29.
  • atTimeZone() tests for every clock.
  • Tests for the removed now(), isFuture(), isPast() and DefaultClock APIs are removed.
  • PHPUnit (7641 tests), PHPStan and ECS all pass.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.27%. Comparing base (81915a3) to head (1c48e3b).

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #133      +/-   ##
============================================
+ Coverage     99.24%   99.27%   +0.03%     
+ Complexity     1143     1131      -12     
============================================
  Files            48       48              
  Lines          2517     2488      -29     
============================================
- Hits           2498     2470      -28     
+ Misses           19       18       -1     

☔ View full report in Codecov by Harness.
📢 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.

@joelbutcher
joelbutcher marked this pull request as ready for review September 28, 2026 16:53
@BenMorel

Copy link
Copy Markdown
Member

Thank you for your PR, @joelbutcher.

Some early comments / questions:

  • Could you remove the PSR Clock implementation for now? I think it would be better to discuss it separately in another PR once this one has landed; in particular, I'd like to discuss creating a separate adapter vs implementing the Psr ClockInterface directly on ZonedClock.
  • Should SystemClock implement Clock or ZonedClock ? Should we provide both a SystemClock and a SystemZonedClock (or as a factory method, ZonedClock::system()) ?
  • Should we remove all now() static methods from date-time classes, and have a factory method for each of them on ZonedClock, not just ZonedDateTime/LocalDateTime/LocalDate/LocalTime, so that the entrypoint to get the current date/time is always the Clock? (ZonedClock being a concrete class here, we don't have the issue of bloating an interface anymore)
  • Should we rename Clock::getTime() to getInstant(), and call the ZonedClock getters getZonedDateTime() / getLocalDateTime() etc. for consistency?
  • Should we add isInPast() / isInFuture() to ZonedClock, to replace isPast() / isFuture() on Instant and ZonedDateTime?

@solodkiy What do you think of this PR as a whole, and do you have an opinion on these points?

@joelbutcher

Copy link
Copy Markdown
Contributor Author

Thanks @BenMorel

On your questions, here are my thoughts:

✅ PSR-20 is removed
I will update the description to match once the commit is pushed! Would be very happy to add PSR-20 support in a follow up PR. Personally, if we're wanting to keep psr/clock out of core code and keep the package focussed on brick/date-time types, then I'd lean more towards providing a PSR adapter. This stops ZonedClock from being the one class returning a native DateTimeImmutable, and gives the reverse direction (PSR clock → brick Clock) a natural home.

SystemClock
I would keep it a implementation of Clock. Making is zoned, brings TimeZone into that classes constructor, it would need to extend the current final class ZonedClock which then calls into question whether ZonedClock should be an interface instead. I'm personally not a fan of this. I would instead recommend builder methods on ZonedClock instead:

$clock = ZonedClock::system(TimeZone $timezone);
// the same as doing:
$clock =  new ZonedClock(new SystemClock, $timezone);

$clock = ZonedClock::fixed(Instant $instant, TimeZone $timezone);
// primarily for tests, basically doing this:
$clock = new ZonedClock(new FixedClock($instant), $timezone);

Removing ::now() static builders

  • Not against this myself
  • makes clocks the main entry point (which is good for dependency injection and unit testing).
  • I would suggesting following Noda Time and only adding getters for the core types (ZonedDateTime, LocalDateTime, LocalDate, LocalTime); the rest are one hop away via LocalDate (getYearMonth(), getYearWeek(), getQuarter(), getDayOfWeek()).
  • We'd probably want to add a LocalDate::getMonthDay().
  • Instant::now($clock) could probably go too, as it's just $clock->getTime().

Renaming to getInstant() / getLocalDate() etc.

  • YES! ->getTime() currently means Instant on Clock and LocalTime on ZonedDateTime which is why I used the getCurrent* prefix.
  • Your naming also matches ZonedDateTime::getInstant(), a win for consistency 😍

isInPast() / isInFuture() on ZonedClock

  • I'm less sure here.
  • Comparing an Instant or ZonedDateTime with now doesn't need a time zone, so moving these onto ZonedClock forces an irrelevant zone on the caller
  • My preference would be to keep them on the value objects with a required clock ($expiresAt->isPast($clock)), keeping only LocalDateTime's on a ZonedClock.

If we go with these, DefaultClock ends up with almost nothing left to serve, so I could fold its removal into this PR rather than a follow-up.

@BenMorel

Copy link
Copy Markdown
Member

Hi @joelbutcher,

SystemClock

I also think that SystemClock should implement Clock; if we made it a ZonedClock, then we'd provide the usual footgun of relying on the system / default timezone, and would make it the default behaviour.

Maybe we can provide an easy way to create a ZonedClock with the system timezone though, but make it very explicit, so that people don't rely on the system timezone by accident; something like:

$clock = new SystemClock()->withTimeZone(TimeZone::fromSystem());

Side note: interestingly, there is an RFC being discussed that adds a native Clock that returns an Instant with no timezone. If this RFC ever gets accepted, then we would be heading in the same direction as native classes.

Removing ::now() static builders

Let's experiment with this.

I would suggesting following Noda Time and only adding getters for the core types (ZonedDateTime, LocalDateTime, LocalDate, LocalTime); the rest are one hop away via LocalDate (getYearMonth(), getYearWeek(), getQuarter(), getDayOfWeek()).

Agreed.

We'd probably want to add a LocalDate::getMonthDay().

Let's do this (can be its own PR, mergeable immediately).

Instant::now($clock) could probably go too, as it's just $clock->getTime().

Yes, if we're taking that direction, then let's remove all now() methods.

As far as I can see, only Year is unreachable indirectly via ZonedClock. Let's accept that we can't have an equivalent to Year::now() for now.

Renaming to getInstant() / getLocalDate() etc.

YES! ->getTime() currently means Instant on Clock and LocalTime on ZonedDateTime which is why I used the getCurrent* prefix.

Let's go! Please remove the Current prefix from ZonedClock getters then.

isInPast() / isInFuture() on ZonedClock

My preference would be to keep them on the value objects with a required clock ($expiresAt->isPast($clock)), keeping only LocalDateTime's on a ZonedClock.

If we're removing ::now(), I don't think we should keep other methods that accept a Clock inside value objects. I think we should head towards a single direction dependency : Clock depends on value objects, not the other way around.

I would love to keep the isPast/isFuture functionality though; I believe it's a plus for readability to be able to do:

$clock->isInFuture($dateTime);

vs:

$dateTime->isAfter($clock->getInstant());

But that means that isIn(Past|Future) would need to go on the Clock interface, which would force all implementations to provide them.

Unless you have better ideas (Clock as an abstract class?), let's just remove isPast/isFuture for now. I'll add this feature again later if I find a better way.

DefaultClock

If we go with these, DefaultClock ends up with almost nothing left to serve, so I could fold its removal into this PR rather than a follow-up.

Let's remove it in this PR!

@joelbutcher joelbutcher changed the title Add ZonedClock, and take it in now() methods Add ZonedClock, and remove now() methods and DefaultClock Oct 2, 2026
@joelbutcher

joelbutcher commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @BenMorel, I've pushed all of this to the branch.

SystemClock

It still implements Clock, and I've removed "it is the default clock" from its docblock. I haven't added TimeZone::fromSystem() yet. Happy to add it here or in a separate PR, whichever you prefer.

Interesting RFC, nice to see native PHP heading the same way.

Removing ::now()

All now() methods are gone, including Instant::now(). The CHANGELOG has a migration line for each one. Year is the only one with no direct replacement: Year::of($clock->getLocalDate()->getYear()).

LocalDate::getMonthDay() is in its own PR: #134.

Renames

  • Clock::getTime() is now getInstant().
  • The ZonedClock getters are now getZonedDateTime(), getLocalDateTime(), getLocalDate() and getLocalTime().

One leftover: FixedClock::setTime(Instant) now sits a bit oddly next to getInstant(). Shall I rename it to setInstant() here as well?

isPast() / isFuture()

I removed them from Instant, LocalDateTime and ZonedDateTime, so value objects no longer depend on clocks.

For a later PR, one way to get the readability back without touching the Clock interface: a small final class that wraps a Clock:

$timeline = new Timeline($clock);

$timeline->isInFuture($expiresAt); // Instant|ZonedDateTime
$timeline->isInPast($createdAt);

Implementers of Clock don't have to provide anything, the dependency still goes clock → value objects, and it's easy to inject or build on the fly. Naming ideas:

  • Timeline: my favourite, $timeline->isInPast($x) reads naturally
  • Now: $now->isAfter($x) is short, but I find isInPast() / isInFuture() awkward on it
  • ClockComparator: very explicit, but a bit heavy

One gap: wrapping a plain Clock can't handle LocalDateTime, since comparing it to now needs a time zone. Two options:

  • leave it out, and let callers write $x->isAfter($zonedClock->getLocalDateTime())
  • make the helper wrap a ZonedClock instead, so it can accept Instant|ZonedDateTime|LocalDateTime; multi-zone apps would build it with $clock->withTimeZone($tz)

I lean towards the second, since it keeps all three types readable, but it does mean single-zone and multi-zone apps use it slightly differently.

What do you think?

DefaultClock

Removed in this PR. Stopwatch now requires a Clock. The README's freeze/travel/scale examples now pass a FixedClock, OffsetClock or ScaleClock to your code instead.

PHPUnit, PHPStan and ECS all pass, and I've updated the PR title and description to match.

@BenMorel

BenMorel commented Oct 3, 2026

Copy link
Copy Markdown
Member

One leftover: FixedClock::setTime(Instant) now sits a bit oddly next to getInstant(). Shall I rename it to setInstant() here as well?

Yes please!

isPast() / isFuture()

Interesting Timeline idea. Let's keep this out of this PR for now, we can discuss it in a later one.

@BenMorel

BenMorel commented Oct 3, 2026

Copy link
Copy Markdown
Member

@joelbutcher Can you please rebase as well?

Comment thread src/Clock.php Outdated
/**
* Returns a clock that reads the current time from this clock, in the given time zone.
*/
public function withTimeZone(TimeZone $timeZone): ZonedClock;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we rename this to inTimeZone()? Withers usually return the same type, whereas this one returns a different type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed that with*() reads wrong here. I went with atTimeZone() rather than inTimeZone(), since that's what the library already uses for the same kind of conversion: Instant::atTimeZone() and LocalDateTime::atTimeZone() also turn one type into another one bound to a zone. Happy to switch to inTimeZone() if you prefer.

joelbutcher and others added 5 commits October 4, 2026 22:32
Clock now only provides the current Instant, like Java's InstantSource
and Noda Time's IClock. The new ZonedClock wraps a Clock and a TimeZone,
like Noda Time's ZonedClock and Java's Clock, and implements PSR-20.

- Clock gains withTimeZone(TimeZone): ZonedClock
- ZonedClock implements Psr\Clock\ClockInterface: now() returns a
  DateTimeImmutable in its time zone, nanoseconds rounded down
- ZonedClock provides getCurrentZonedDateTime(), getCurrentLocalDateTime(),
  getCurrentLocalDate() and getCurrentLocalTime()
- New runtime dependency: psr/clock ^1.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUshm9CLUVJZrwuV4jA8q4
now(TimeZone $timeZone, ?Clock $clock = null) becomes now(ZonedClock $clock)
on DayOfWeek, LocalDate, LocalDateTime, LocalTime, MonthDay, Quarter, Year,
YearMonth, YearWeek and ZonedDateTime, as do LocalDateTime::isFuture() and
isPast().

The time zone stays required, as a ZonedClock cannot exist without one,
and the clock becomes explicit:

    LocalDate::now($this->clock);
    LocalDate::now($clock->withTimeZone($timeZone));

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUshm9CLUVJZrwuV4jA8q4
PSR-20 support will be discussed separately, including whether it should
be implemented directly on ZonedClock or through an adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiFf5jYdYJDg1jRMHBoFRY
- Rename Clock::getTime() to getInstant()
- Remove the Current prefix from ZonedClock getters
- Remove all now() methods: use a ZonedClock (or Clock::getInstant()) instead
- Remove isPast() / isFuture() from Instant, LocalDateTime and ZonedDateTime
- Remove DefaultClock; Stopwatch now requires a Clock

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…() to setInstant()

atTimeZone() matches Instant::atTimeZone() and LocalDateTime::atTimeZone(),
which also return a different type.

Also update a ZonedClockTest expectation for the full seconds now emitted by
ZonedDateTime::toISOString() (brick#131), and point the MonthDay::now() migration
to LocalDate::getMonthDay() (brick#134).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joelbutcher

Copy link
Copy Markdown
Contributor Author

Done, and rebased on main.

PHPUnit (7641 tests), PHPStan and ECS all pass.

@BenMorel

Copy link
Copy Markdown
Member

I'm quite happy with the state of this PR now, and would appreciate feedback from @solodkiy, @gnutix, @zjbarg, @nkamuo, @brunocassol here.

TL;DR:

  • no more DefaultClock
  • no more ::now() methods
  • you always start from a Clock:
$clock = new SystemClock();
$zonedClock = $clock->atTimeZone(TimeZone::utc());

$today = $zonedClock->getLocalDate();

Biggest advantages I see:

  • no more "global clock"
  • applications that deal with a single timezone can inject a ZonedClock
  • applications that deal with multiple timezones can inject a Clock and call atTimeZone() dynamically

Thank you 🙏

@gnutix

gnutix commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Claude tried the PR on our Symfony project (multi-tenant, one database and one time zone per tenant, a mix of modern DDD code and a legacy codebase), by upgrading to this branch and migrating everything until the whole test suite was green again. It works for us. The migration took about 300 ::now() calls across ~450 files. Nothing was blocked by the new API; all the cost came from code that had relied on reaching "now" globally.

What it did

  • Services get the clock injected. Since each tenant has one time zone that lives in its configuration, we register a lazy ZonedClock service built from it, so X::now($tenantTimeZone) becomes $this->clock->getLocalDate(). Code that needs another zone (mostly UTC) injects a Clock and calls atTimeZone(). That's exactly the single-zone / multi-zone split you describe, and both sides felt natural.
  • Entities and value objects can't receive services, so their named constructors and mutators that stamp a creation or update time now take an explicit Instant $now, and their callers pass it from their own clock. This was the bulk of the work (~60 entities), but it's mechanical and makes these methods deterministic, so it's better.
  • A frozen "now" per request. We used to freeze DefaultClock at kernel boot, so a whole request saw a single instant. We now register a FixedClock frozen the first time it's read as the default Clock service. The few places that measure elapsed time explicitly ask for the SystemClock. That distinction used to be invisible, and the PR forces us to make it, which is a plus.
  • Legacy code reads the clock from its service container. Same for the few places a framework calls without giving us a way to pass a clock: a workflow marking-store setter, validation callbacks on DTOs, and argument-less getters that a framework reads. They end up with a service locator or a context array. That's our problem, not the library's, but it's worth knowing that this is where a migration hurts.
  • Tests: unit tests build a FixedClock / ZonedClock and pass it in. In functional tests, the framework reboots the kernel between requests, so a clock frozen in the container doesn't survive. We added a small test-only holder that keeps the frozen instant until the end of the test. In a sense it's the only "global clock" left.

Small frictions it noticed

  1. No Instant::parse(). Writing a readable fixed instant in tests means ZonedDateTime::parse('2026-10-10T08:00:00Z')->getInstant() or a raw epoch. Now that every test builds its own clock, this comes up constantly. Instant::parse() (ISO 8601 with Z or an offset) would help a lot.
  2. A fixed ZonedClock at a given local date-time names the zone twice: (new FixedClock($localDateTime->atTimeZone($zone)->getInstant()))->atTimeZone($zone). Not a big deal, but a shortcut from a ZonedDateTime would read better in tests.
  3. "Today in UTC" is now $clock->atTimeZone(TimeZone::utc())->getLocalDate(). Fine, just a bit long for something that common.
  4. +1 on the PSR-20 follow-up. We had to bridge our Clock to a PSR-20 clock by hand for a JWT library's validation.

Thanks for your work, it's a nice improvement!

@BenMorel

Copy link
Copy Markdown
Member

Thank you, @gnutix, that's a very useful feedback!

@gnutix

gnutix commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Let me know if you want me to run some Claudes on the "small frictions" (just give me some direction what you'd like for these first).

joelbutcher and others added 3 commits October 27, 2026 13:24
Parses an ISO 8601 date-time with an offset, such as 2026-10-10T08:00:00Z,
which makes fixed instants in tests readable without going through
ZonedDateTime::parse(...)->getInstant() or a raw epoch second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQhVM8scWxQrMU3djngjpv
Creates a ZonedClock fixed at a given ZonedDateTime, in its time zone,
so tests no longer need to name the time zone twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQhVM8scWxQrMU3djngjpv
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQhVM8scWxQrMU3djngjpv
@joelbutcher

Copy link
Copy Markdown
Contributor Author

@gnutix I've addressed most of those frictions.

RE: "Today in UTC": The code is actually

$clock->withTimeZone(TimeZone::utc())->getCurrentLocalDate()

The long form is only needed when the clock you have is set to a different time zone. An app that works in UTC can inject a UTC ZonedClock and call $clock->getCurrentLocalDate().

@joelbutcher

joelbutcher commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

@BenMorel I've synced the branch with main, though CI is failing which seems to be related to the Ubuntu's image upgrade to 26.04, which moved legacy timezone names like Asia/Rangoon and CET into a separate tzdata-legacy package that's likely missing from PHP's system timezone data - unrelated to our changes

ubuntu-latest now points to Ubuntu 26.04, which no longer installs
tzdata-legacy. Without it, PHP doesn't know legacy time zone names such as
CET and Asia/Rangoon, which the tests use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQhVM8scWxQrMU3djngjpv
@joelbutcher

Copy link
Copy Markdown
Contributor Author

Quick follow-up on my two comments above:

  • @gnutix: my snippet used the old names. On the current branch it's $clock->atTimeZone(TimeZone::utc())->getLocalDate(), and $clock->getLocalDate() for an app that injects a UTC ZonedClock. Instant::parse() and ZonedClock::fixed() are in too.
  • @BenMorel: CI is green again. ubuntu-latest now runs Ubuntu 26.04, which no longer installs tzdata-legacy, so PHP didn't know legacy names like CET and Asia/Rangoon. I confirmed it by pinning ubuntu-26.04: the tests fail without the package and pass with it. The fix is 1c48e3b, a CI step that installs it. Main will hit the same failure on its next run, so I'm happy to move that commit into a separate PR if you'd rather keep this one focused.

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.

3 participants