-
v0.9.0
Stablereleased this
2026-08-09 22:10:26 -05:00 | 91 commits to main since this releaseRepeating events. Creating them, and changing or deleting this occurrence, this and
every later one, or all of them — including the this-and-following split that had
been held back for 0.9.1.Added
-
create_eventcan repeat, from a constrained vocabulary rather than a rawRRULE:
repeat(daily, weekly, monthly, yearly),repeat_interval,repeat_daysfor a weekly
rule, and eitherrepeat_countorrepeat_until. Give neither and it repeats
indefinitely, which is the right answer for a standing weekly meeting.Not a raw rule string, deliberately.
FREQ=SECONDLYwith noCOUNTis legal
iCalendar, expands to millions of occurrences, and turns every later read of that
calendar into a timeout. A vocabulary that cannot express it beats a validator that has
to catch it. -
scopeonupdate_eventanddelete_event—this,following, orall.
Required for a repeating event, with no default. "Cancel Tuesday's standup" means
one occurrence to a person; under a default ofallit ends the standup permanently.
There is no safe default, only a convenient one, so a missing scope is refused with the
three choices named andscope_required: truein the envelope. A scope on a one-off
event is accepted and ignored — it has one occurrence, so every scope names the same
write. -
Every occurrence of a series now carries an
event_id. Before this, repeating
events had none, because every write against them was refused. The handle gained a
fourth field naming which occurrence it is — the value aRECURRENCE-IDcarries, in
UTC so it survives the server'sTZchanging between being issued and being used.
Shorter handles still decode, so a one-off event's handle is byte-identical to the one
0.8.1 issued.What the absence of a handle means has narrowed accordingly: a subscription, or a
calendar that is readable but not writable.
The three scopes are three different operations
Not three flags on one operation. Removing one occurrence of a series is a write, not a
deletion — and that is the whole safety argument here:Scope Update Delete thisadd a RECURRENCE-IDoverride to the same resourceadd an EXDATEto the masterfollowingtruncate the original with UNTIL, create a new resource for the remainderset UNTIL— one write, no second resourcealledit the master DELETEthe resourceOnly
all, andfollowingfrom the very first occurrence, are aDELETE. An
implementation that reaches forobj.delete()because the caller said "delete" takes the
entire series and gets exactly the same 204 back as taking one occurrence, so nothing
downstream can tell.Two more consequences worth stating:
- An override is built by copying the master, properties and VALARM reminders and all,
never composed from the fields this server models. An occurrence becoming an override is
the one moment a reminder could silently disappear, and a lost reminder is invisible in
every response until an appointment does not ring. followingfrom the first occurrence, and truncation generally, degenerate. An
UNTILearlier thanDTSTARTwrites a rule that generates nothing — a resource that
exists and renders as no events at all. That case is detected and treated as the whole
series instead.
The split creates before it truncates
Editing
followingis the only operation here that writes two resources, and CalDAV
offers no transaction across them. One of the two writes can therefore succeed alone, and
the order decides which failure the user gets:- remainder first, then truncate — a failure between them leaves a visible duplicate
series from the split date onward, recoverable by hand; - truncate first — a failure leaves every future occurrence silently gone, behind a
plausible-looking success.
So the remainder is created first, and if the truncation then fails the error says
precisely that both are on the calendar and nothing was lost, rather than "nothing was
modified", which would be a lie.A series bounded by
COUNTrefuses afollowingedit. Splitting it correctly means
knowing how many occurrences fall before the split, which needs the series expanded — and
a count wrong by one is a missing or extra occurrence months away with nothing to indicate
it. Deletingfollowingon a counted series is fine: every occurrence being kept was
inside the original count either way.Changed
-
recurringis read from the resource, not from the component. ARECURRENCE-ID
override carries noRRULE, so asking the expanded copy reported a moved occurrence of
a weekly meeting as a one-off. All of a series' components live in one resource (RFC
4791 §4.1), so asking the resource cannot get it wrong. The flag is now correct for an
override rather than merely not trusted. -
The write instructions and both tool descriptions now carry the scope and its lack of a
default. A model told only that series are "supported" picks a scope itself.
Testing
417 tests, and a twelve-mutation pass over the safety properties — each one
individually reverted, each one caught. The three that were not caught on the first run
became tests before this shipped, and all three were real gaps: a reminder surviving an
occurrence becoming an override, an override not inheriting the series rule, and a second
edit of the same occurrence editing that override rather than adding another.Recurrence has not run against the live account.
docs/plan.mdcarries the eight
checks, which want a scratch calendar — one of them deliberately tries to take a whole
series.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v0.8.3
Stablereleased this
2026-08-09 20:53:43 -05:00 | 97 commits to main since this releasecreate_eventandlist_eventsdescribed the same all-day event two different
ways. Found by running A9 against the live account.Fixed
-
An all-day
endis reported the same way by every tool. The same event,
in two responses:create_event → end: 2026-08-10T23:59:59.999999-05:00 (inclusive last day) list_events → end: 2026-08-11T00:00:00-05:00 (exclusive boundary)build_veventnormalises an all-day end to the exclusive next-dayDATE, but
create_eventbuilt its reply from the arguments it was handed rather than
from the document it had just written, so it echoed the input convention while
every read reported the stored one. Nothing in either response said which was
in hand.The round trip is what made it a footgun rather than a cosmetic difference:
take an end from a response, shorten it to a date, hand it back as the last
day, and the event grows by one — on every edit, silently.create_eventnow parses back the document it wrote, through the same
parse_singlethat reads. Not a patch to the arithmetic: one code path, so
the two cannot drift apart again. Responses are exclusive everywhere, and
handing a reportedendstraight back is now exact.
Changed
-
create_event's description says a timed event must carry a length.end
andduration_minutesare both optional in the schema — because which one
you send is a choice — but omitting both is an error, and nothing said so up
front. It now does, and tells the model to ask rather than invent a duration. -
It also states that responses report
endexclusively, and to pass the
value back verbatim rather than shortening it to a date.
Testing
The fake now makes a created event readable afterwards, which is what lets a
test compare whatcreatesaid against what a later read reports. That
comparison was impossible to express before, which is why three releases shipped
with the two disagreeing. Reverting the fix fails three tests.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v0.8.2
Stablereleased this
2026-08-09 20:34:33 -05:00 | 98 commits to main since this releaselist_eventscould return fewer events than the day held, with no error.Fixed
-
Expansion happens per resource instead of per calendar. Every resource on
a calendar was merged into a single VCALENDAR before expanding, and two
resources whose events share a UID collapsed into one occurrence:3 resources -> 3 events [three distinct uids] 3 resources -> 1 events [same uid, different times] 3 resources -> 2 events [two share a uid]RFC 4791 §4.1 requires a calendar object resource to hold exactly one UID, so
the resource is the correct unit of expansion — merging across resources
merged things the spec calls distinct. Duplicate UIDs across resources are
themselves a violation, but they occur; caldav's own iCloud notes list
duplicate_in_other_calendar_with_same_uid_breaks.Found because a delete made the same query in the same window return more
events than it had a moment earlier — 3 resources reporting 1 event, then 2
reporting 2. Nothing raised. This is the third defect of that shape in this
project and the second to survive a green suite.The merge existed to cover servers that split a series'
RECURRENCE-ID
overrides across resources. No compliant server does and iCloud does not, but
the trade is worth naming: on one that did, this would show the master's
occurrence un-overridden and the override, as two events. -
A resource that fails to expand now costs only itself. The expansion was
wrapped once around the whole calendar and returned no events at all on
failure, so one malformed series could empty a day. That is the isolation
parse_componentsalready applied to a resource that will not parse.
Changed
-
tool/eventslogs both filters — the calendar list, and the query's
length together with how many events it matched from. A short count has two
very different explanations, and the line carrying only the window could not
tell a search that correctly matched one event from a read that lost two.
Diagnosing the defect above needed exactly that distinction and the line did
not carry it.The query's length, never its text: it is user input and this line goes to
Portainer.
Docs
docs/runbook.mdanddocs/oauth-persistence.mdare gone. Roughly a thousand
lines of them were✅ DONEphase narrative describing a deployment that now
exists. What was still live — the outstanding live checks, the failure-mode
table, adding a second tenant, backing out — moved todocs/plan.md,
which also carries the 0.9.0 recurrence design and the decisions behind it.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v0.8.1
Stablereleased this
2026-08-09 19:53:19 -05:00 | 99 commits to main since this releaseupdate_eventanddelete_eventdid not work against iCloud at all. Every call
failed on the lookup, before touching anything.Fixed
-
An event is now fetched by its resource URL instead of searched for by UID.
0.8.0 resolved a handle with caldav'sget_event_by_uid, which issues a
calendar-queryREPORT filtering on UID. iCloud answers that with 412
Precondition Failed — caldav's own source lists
get_object_by_uid_is_brokenamong the iCloud quirks — so both write tools
failed at_load_for_writeon every call:ERR caldav/write > could not load event for writing | error=ReportError at '412 Precondition Failed' ERR tool/delete > delete_event failed | error=CalendarUnavailable: Could not read that event backNothing was ever modified, and the error envelopes reported that correctly —
the failure was total rather than partial, which is the one thing that went
right. A CalDAV resource is addressed by URL, and the href came back with the
read that issued the handle in the first place; searching for it again was
doing the work twice, through the one query shape this server cannot rely on. -
event_idnow carries the resource filename as a third field. The
encoding is internal and no tool signature mentions its contents, so this is
not a breaking change to the API — but handles issued by 0.8.0 name no
resource and are refused with "list the events again" rather than resolved.
Since no 0.8.0 handle could ever be written through, nothing that worked
before stops working.The filename only —
ABC-123.ics, never the path it hangs off, which is
/<dsid>/calendars/<collection>/and carries the account identifier. The URL
is rebuilt server-side against the calendar the allowlist already resolved, so
a handle still cannot point a write at a calendar it does not name. -
An event whose resource filename is unknown gets no
event_id, which
extends the existing rule rather than adding one: a handle that cannot be
resolved is worse than no handle, and absence already means "not editable" for
repeating events, subscriptions and read-only calendars. -
A resource that no longer holds the event it was read as is refused. An
href can be reused after a delete, and editing whatever moved in — under the
old event's title — is the one outcome worse than failing.
Testing
The fake CalDAV handle now raises 412 on
get_event_by_uidthe way iCloud
does, so the 0.8.0 bug fails the suite instead of passing against a fake more
capable than the real server. Reverting the fix fails 17 tests, six of them new,
including a full read-to-write round trip —list_events, take theevent_id
off an event, delete it — which nothing covered before. That gap is exactly how
a total outage of both write tools shipped green.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-09 16:44:19 -05:00 | 100 commits to main since this releaseEditing and deleting, addressed by an opaque handle. 0.7.0 could put things on
the calendar and never take them off again.Added
-
update_event— changes the fields it is given on one stored event and
leaves the rest alone. It mutates the stored resource rather than rebuilding
it, which is the difference between an edit and a quiet amputation: a
rebuild from the handful of fields this server models would discard the VALARM
reminders, attendees, categories and Apple X-properties it does not, and
nothing in the response would say so.Moving an event preserves its length, so a start on its own is enough. Whether
an event is all-day is read from the stored event, never from the shape of the
argument — converting a day off into a 30-minute appointment because a caller
passed a time is not a thing a caller should be able to do by accident. -
delete_event, which requiresexpected_titleand refuses on a mismatch.
It costs one argument the caller already has, and it converts the failure that
cannot be undone — two ids transposed out of one response — into a refusal
that names both titles. The response carries the whole deleted event, since
that is the only remaining record of what was there. -
event_id, an opaque handle on every addressable event. Not the resource
URL, which carries the account DSID and must never reach the model; not the
uid, which every occurrence of a series shares; and not(calendar, uid)as
two arguments, which lets a model pair the right uid with the wrong calendar
or assemble one from parts it half-remembers. One string, produced by a read,
passed back verbatim.Its absence is the signal. No
event_idmeans nothing here can be edited
or deleted: repeating events, subscriptions, and everything on a calendar that
is readable but not writable. WithWRITABLE_CALENDARSunset no event carries
one at all, so the read surface still matches the read-only releases exactly.Deliberately unsigned. The write allowlist is checked against the calendar
named inside the handle before iCloud is contacted, so forging one buys
access to exactly the calendars the operator already opted in to. Signing
would add a key with theMCP_OAUTH_SIGNING_KEYfailure mode — rotate it and
every handle issued earlier in a conversation stops resolving, which reads as
the events having vanished.
Changed
-
Every error envelope out of a write tool now states
created,updatedor
deletedasfalse. They were present on the failures raised from the
client and absent on the ones caught during argument validation. A missing key
is not the same signal as a false one, and the gap was on exactly the paths
where the model has the least other evidence. -
The server instructions name the new capabilities and their limits together,
because stating the capability alone invites the model to offer the rest.
Security
-
Repeating events are refused, and the refusal is checked against the stored
resource rather than against anything a read inferred. ARECURRENCE-ID
override carries no RRULE of its own, so a moved instance of a weekly meeting
reads as a one-off — and editing it by UID resolves to the resource holding
the whole series. Deleting is the same story with a worse ending: one DELETE
against a series master removes every occurrence, past and future, and returns
the same 204 as removing a single event. -
A concurrent edit is refused, not overwritten. caldav sends
If-Match
from the ETag of an object it loaded, so an event changed on a phone between
the read and the write comes back 412 and is left alone. -
Upgrading grants deletion to anyone who had
WRITABLE_CALENDARSset for
0.7.0, with no configuration change. This is the roadmap's shared gate
working as agreed, and it is worth saying out loud: the opt-in that previously
meant "may add events" now also means "may remove them". Clearing the variable
is still the off switch.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-09 15:27:21 -05:00 | 103 commits to main since this releaseFirst release that can change the calendar. Everything before this was read-only by
construction; from here it is read-only by configuration, and the configuration
defaults to read-only.Added
-
create_event— creates one non-recurring event on a calendar named in the new
WRITABLE_CALENDARSallowlist. Accepts a finish time as eitherendor
duration_minutes, handles all-day events, and echoes the whole created event back
rather than an acknowledgement, so a timezone or all-day misunderstanding shows up in
the transcript instead of on someone's phone a day later. -
WRITABLE_CALENDARS, empty by default — and empty means the tool is not
registered, not that it refuses. An offered tool is one the model will promise the
user before it discovers it cannot deliver, so with this unset the served tool surface
is identical to 0.6.0.Separate from
CALENDARSrather than a flag on it: reading a shared household
calendar to answer "when is everyone free" is not consent to write to it. -
A duplicate guard. A create whose response the caller never saw — a timeout on the
write — leaves the event created and the model convinced it was not, and the retry
books the meeting twice. An event with the same title and start is refused, and the
refusal carries the existing event so the user can be told what is already booked.
allow_duplicate=trueoverrides it. Best-effort by design: if the check cannot read
the calendar the create still proceeds, because turning iCloud throttling into an
inability to add anything is a worse failure than the one being prevented. -
A
writablepreflight check andwritable_calendarson the boot line. A name in
the allowlist matching no readable calendar registers a tool that refuses every call,
which is invisible otherwise. Reported as a failure but never fatal — a typo in the
write allowlist must not take the read path down with it. -
mcp_gateway/calendar/compose.py, the inverse ofexpand.pyand network-free for
the same reason. Two things in it are the ones worth getting right:DTENDis
exclusive, so a one-day all-day event ends on the following date — writing the same
date twice yields a zero-length event most clients render as nothing at all. And timed
events are written in UTC rather than with aTZID, because aTZIDreference is
only legal alongside a matchingVTIMEZONEcomponent; these are single non-recurring
instances, so there is no future DST transition for a zone rule to get right.
Changed
-
Calendar.read_onlyreports the truth instead of a hardcodedtrue. It is how
the model knows which calendar to offer. Subscriptions are always read-only — a feed is
an HTTPS GET against someone else's server, and naming one inWRITABLE_CALENDARS
does not change that. -
The server instructions name the writable calendars and, just as explicitly, the
limits: nothing can be edited or deleted, and repeating events cannot be created.
Stating the capability without the limits invites the model to offer the rest.
Security
-
The credential was never the boundary and still is not. An app-specific password
has granted full CalDAV read and write since the first release; what changed is that
this code will now use it.WRITABLE_CALENDARSis the boundary, it is enforced before
iCloud is contacted, and it is checked before the calendar name is even looked up — so
a real calendar that is merely not writable does not come back as "no such calendar". -
Resource URLs are never logged or returned. They carry the account DSID, and tool
output reaches the model.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-09 13:35:06 -05:00 | 104 commits to main since this releaseAdded
-
OAuth state now persists across redeploys. The connector was signed out by every
single stack update.FASTMCP_HOMEis set explicitly to/var/lib/fastmcp— in the
image and in the stack — and the stack mounts an NFS volume there, so the encrypted
file storeOAuthProxywas already writing survives a service update.Set explicitly rather than left to
platformdirs, because the default is derived from
the container user and$HOME: a base-image change would relocate the store silently,
and a moved store is indistinguishable from a lost one. The volume is inline rather
thanexternal: truefor the reasonnamed_volumes_guide.mdgives — Portainer here
cannot target a node and there is no SSH to the Pis, so an external volume exists on
exactly one node, and a task landing anywhere else gets an empty one. That would bring
the sign-out back intermittently, which is worse than constantly. It sits on
volume1(NVMe) rather thanvolume2(HDD): a tree of tiny files rewritten on every
token refresh is exactly the small-random-write case the guide reserves the NVMe for.This does not raise the replica ceiling. A file store is per-container-filesystem,
soreplicas: 1still stands underAUTH_MODE=oauth. Seedocs/oauth-persistence.md. -
An
oauth_storagepreflight check, andfastmcp_homeon the boot line.
OAuthProxymkdirs its storage in its constructor, so an unwritable path was a
PermissionErrorduring app construction — a crash loop whose traceback named
fastmcp,pathliband a hashed directory, and mentioned neither the volume nor its
owner. That is the likely first failure here: thelocalNFS driver never contacts the
server until a container mounts it, so a missing export directory or wrong ownership
produces a perfectly healthy-looking volume and fails only at start. The check runs
before the app is built and names it in one line. An unsetFASTMCP_HOMEis advisory,
not a failure — it is legal, just drift waiting to happen.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-09 11:08:34 -05:00 | 105 commits to main since this releaseChanged
-
CalDAV now stays on TCP; HTTP/3 is disabled for the iCloud session. iCloud answers
withAlt-Svc: h3=":443", and caldav's HTTP layer acts on it: measured against the live
host, request 1 negotiates HTTP/1.1 and every request after it negotiates HTTP/3 over
UDP 443. That is fine on a network carrying UDP 443 cleanly and invisible on one that
is not — a blocked or MTU-broken UDP path returns no error, only a request that never
comes back. HTTP/3 buys a handful of small CalDAV requests nothing, so the dependency
is removed rather than diagnosed.Framed honestly: this is hardening, not a fix for a reproduced bug. The timeouts
seen while investigating stopped without it. -
CI no longer cancels a superseded run.
cancel-in-progresswas cancelling
publishes, not just tests: two pushes a minute apart killed the first one'simage
job partway through, so that commit's:<sha>tag — the only immutable rollback target
CI produces — never reached the registry, and the window where:lateststill pointed
at the previous version got wider. Superseded runs now queue. The cost is runner time
re-proving a commit nobody will deploy; the alternative loses artifacts that cannot be
recreated without pushing an empty commit.
Fixed
- The explanation for signing in again after a redeploy was wrong, in the runbook, in
server.pyand in the stack file.OAuthProxybuilt without aclient_storagedoes
not fall back to memory — it builds an encrypted file store under
$FASTMCP_HOME/oauth-proxy/<key-fingerprint>/. The state is written to disk and would
survive a restart; it is lost because that path sits in the container's writable layer
with nothing mounted there. The fix is therefore a volume, not a database, which is a
much smaller change than the one previously written down.docs/oauth-persistence.md
carries the plan, including why it still does not unblockreplicas > 1.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-09 09:53:14 -05:00 | 109 commits to main since this releaseChanged
-
Calendars are now searched concurrently rather than one after another. Serially, a
slow calendar added its latency to every calendar behind it: on 2026-08-09 iCloud
stalled three of four calendars at the 30-second read timeout, so a single tool call
spent 90 seconds before returning a partial answer — well past the point the caller
gives up, and indistinguishable from a hang. Concurrently the worst case is the slowest
single calendar. Capped at 4 in flight, deliberately: enough parallelism to trip
iCloud's throttling would trade one slow calendar for four rate-limited ones. -
A timed-out calendar is retried once before being reported unavailable. A read
timeout against iCloud is usually throttling rather than an outage, and it previously
cost the entire calendar for the length of the cache TTL. Only transport-level failures
are retried — retrying a 403 or a malformed response just doubles the latency of a
failure that was never going to succeed. caldav's own 429/503 handling is enabled at
the same time, capped at 10 seconds so a server-suppliedRetry-Afterof an hour
cannot hold a tool call open for one. -
The CalDAV timeout is now set explicitly and configurable (
CALDAV_TIMEOUT,
default 60s). It was previously whatever niquests defaults to — 30 seconds, a number
that first appeared in a production stack trace and had to be traced back to a library
to find out whether anyone had chosen it. It appears on the boot line.
Added
- A log line before the network work starts, naming the window and how many calendars
are being read. The existinglisted eventsline carries the window too, but it is
only reached once every calendar has answered — so for exactly the requests that hang,
the one field that explains why was the one never written.
Fixed
- Warnings rendered uncoloured and unabbreviated in Portainer. The viewer matches
debug/info/warn/errorcase-sensitively, and the formatter emitted
record.levelname.lower()— which iswarningfor Python'sWARNING, one letter off
the token.debug,infoanderroragree by coincidence, which is what hid it: the
one level whose entire purpose is to be noticed was the only one rendering plain. That
covered every warning this service has emitted, including the twoCALENDARSallowlist
warnings that exist specifically to be seen. Now an explicit mapping, withcritical
folded intoerrorbecause the viewer knows five tokens and that is not one.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-08-09 09:53:14 -05:00 | 110 commits to main since this releaseAdded
-
Read-only iCalendar subscriptions (
ICS_FEEDS), fetched over HTTPS and merged into
the existing tools. A subscribed calendar is not stored in the iCloud account — every
device polls the URL independently — so CalDAV never returns one and noCALENDARS
value could ever reach it. That is why Apple's US Holidays was missing rather than
filtered: the account only ever offered six collections and it was not among them.
Feeds go through the sameexpand()as CalDAV data, so an occurrence from a
subscription is indistinguishable downstream, including recurrence and all-day
handling. Verified against Apple's realUS_en.ics: 45 events in 2026, July 4 spanning
exactly one day.Fetching the feed rather than importing it into a real calendar is the point — the file
currently runs to Dec 2029, and an import would quietly stop returning holidays when it
ran out, with nothing to say why.Failure modes here nearly all produce an empty calendar rather than an error, so each
is made loud: an undecoded gzip body (iCloud serves gzip whether asked or not), an HTML
error page served as 200, a body over 5 MB, a non-http(s)scheme (urlopenwill read
file://quite happily), and a feed that is simply unreachable — which reports the
calendar as unavailable rather than empty, and logs an advisory at boot.Not filtered by
CALENDARS: configuring a feed is already the act of opting in, and the
name is not knowable until after the first fetch. -
A
subscriptionspreflight check, present only when feeds are configured. Advisory
rather than fatal — a third-party URL being down is no reason to stop serving the
account's own calendars. -
The boot log now names the calendars the allowlist hid, not just how many.
skipped_by_allowlist=2answers "am I seeing everything?" with "no" and stops there,
and the only other way to learn the names was to run once withCALENDARSempty —
deliberately reading every subscribed collection on the account to find out which ones
to exclude. A calendar created on the phone now shows up in this line. -
A
CALENDARSentry that matches nothing is now warned about individually. The
existing warning only fired when every entry was wrong; one bad entry among several
good ones started cleanly and quietly never consulted that calendar, which is
indistinguishable from an empty calendar at the tool layer. The realistic cause is
emoji — a warning sign is two codepoints (U+26A0 U+FE0F) and the bare U+26A0 renders
identically without matching.
Fixed
- CI's
gatejob ran on Python 3.11 while the container runs 3.12, so nothing
exercised the interpreter production actually uses. It runs on 3.12 now, and
requires-pythonand mypy'spython_versionmoved to 3.12 to match — a floor nothing
tests is a claim nobody checks, and one version everywhere is simpler than two. The
package is not published anywhere, so nothing downstream cares about the floor.
Dropping the 3.11 leg also halves CI wall-clock: there is one shared runner, so matrix
legs ran sequentially rather than in parallel.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-