Skip to content

Fix schedule_rruleset date-based end_on failing with a UTC UNTIL error#171

Merged
cigamit merged 2 commits into
ctrliq:mainfrom
blaipr:fix-rruleset-until-timezone
Jul 11, 2026
Merged

Fix schedule_rruleset date-based end_on failing with a UTC UNTIL error#171
cigamit merged 2 commits into
ctrliq:mainfrom
blaipr:fix-rruleset-until-timezone

Conversation

@blaipr

@blaipr blaipr commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

In plugins/lookup/schedule_rruleset.py, when a rule's end_on is a date (rather than a count), the plugin builds the rule with a naive UNTIL value while splicing ;TZID=<zone> into the first rule's DTSTART. The plugin then sanity-checks its own output with dateutil.rrule.rrulestr, and dateutil (>= 2.7, the plugin's stated minimum) rejects a naive UNTIL paired with a timezone-aware DTSTART:

RRULE UNTIL values must be specified in UTC when DTSTART is timezone-aware

This meant any rule with a date-style end_on failed, regardless of which timezone was used, including the default. Count-style end_on (a positive integer string) was unaffected.

I fixed this by interpreting the parsed end_on date in the rule's timezone and emitting UNTIL as UTC in the RFC 5545 form dateutil expects (UNTIL=YYYYMMDDTHHMMSSZ), while leaving the rrule.rrule() construction itself unchanged (it still uses the naive value internally, matching the naive DTSTART, so no other behavior changes).

The sibling schedule_rrule.py plugin emits the same naive UNTIL but does not sanity-parse its own output, so it is unaffected by this bug and I left it untouched.

Verification

Reproduced the bug on origin/main by loading the lookup plugin directly and calling run():

--- date end_on, explicit timezone ---
FAILED: AnsibleError Failed to parse generated rule set via rruleset RRULE UNTIL values must be specified in UTC when DTSTART is timezone-aware
--- date end_on, default timezone ---
FAILED: AnsibleError Failed to parse generated rule set via rruleset RRULE UNTIL values must be specified in UTC when DTSTART is timezone-aware
--- count end_on ---
OK: ['DTSTART;TZID=America/New_York:20260101T000000 RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1']
--- no end_on ---
OK: ['DTSTART;TZID=America/New_York:20260101T000000 RRULE:FREQ=DAILY;INTERVAL=1']

After the fix, the same calls:

--- date end_on, explicit timezone ---
OK: ['DTSTART;TZID=America/New_York:20260101T000000 RRULE:FREQ=DAILY;UNTIL=20260110T050000Z;INTERVAL=1']
--- date end_on, default timezone ---
OK: ['DTSTART;TZID=America/New_York:20260101T000000 RRULE:FREQ=DAILY;UNTIL=20260110T050000Z;INTERVAL=1']
--- count end_on ---
OK: ['DTSTART;TZID=America/New_York:20260101T000000 RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1']
--- no end_on ---
OK: ['DTSTART;TZID=America/New_York:20260101T000000 RRULE:FREQ=DAILY;INTERVAL=1']
  • dateutil.rrule.rrulestr parses the generated string without error.
  • Occurrences actually stop at the intended local-time boundary. For end_on: '2026-01-10' with timezone='America/New_York' starting 2026-01-01 00:00:00, the last occurrence is 2026-01-10 00:00:00-05:00 (10 occurrences total), matching midnight local time on the end date.
  • Verified with timezone='UTC' too: last occurrence is 2026-01-10 00:00:00+00:00.
  • Verified a DST-spanning ruleset (start 2026-03-01, end 2026-03-20, America/New_York, with an EXRULE also using a date end_on): the UTC offset correctly shifts from -05:00 to -04:00 across the March DST transition, and the exclusion rule still works.
  • Count-style end_on and rules without end_on are unaffected.
  • ruff check plugins/ passes.

blaipr added 2 commits July 10, 2026 22:43
Any rule using a date-style end_on failed sanity parsing because the
generated UNTIL value stayed naive while DTSTART picked up a TZID,
which dateutil rejects. Convert the local end date to UTC before
emitting it as UNTIL.
Wrap the webhook_key description line to stay within the 160-character
pep8 limit enforced by ansible-test sanity and ruff.
@blaipr
blaipr force-pushed the fix-rruleset-until-timezone branch from 3fd874c to a6dd3d6 Compare July 10, 2026 20:46
@blaipr

blaipr commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on current main. This also picks up a fix for the E501 line-too-long in workflow_job_template.py (line 120, webhook_key description was 163 chars, over the 160-char limit) that is currently breaking CI on main. Ready to merge.

@cigamit
cigamit merged commit f7a7471 into ctrliq:main Jul 11, 2026
3 of 13 checks passed
cigamit pushed a commit that referenced this pull request Jul 11, 2026
)

The pytz-to-zoneinfo migration in #171 missed replacing the pytz call
on the UNTIL timezone conversion line, leaving an undefined name that
breaks both ruff (F821) and ansible-test sanity (pylint).

Use ZoneInfo and datetime.timezone from the stdlib instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants