666ghj/MiroFish · error · StarHistoryError
clock must return a UTC datetime
Error message
clock must return a UTC datetime
What it means
_normalize_now (also used by _format_state_timestamp) received a datetime from the injected clock that was naive (tzinfo None) or had a non-zero UTC offset, and raised StarHistoryError('clock must return a UTC datetime'). Every timestamp the script writes into state goes through this, so the clock callable must supply tz-aware UTC.
Source
Thrown at scripts/star_history.py:334
def _parse_state_timestamp(value: Any, label: str) -> datetime:
if not isinstance(value, str) or not STATE_TIMESTAMP_RE.fullmatch(value):
raise StarHistoryError(f"{label} must use YYYY-MM-DDTHH:MM:SSZ")
try:
parsed = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
except ValueError as exc:
raise StarHistoryError(f"{label} is not a valid UTC timestamp") from exc
return parsed
def _format_state_timestamp(value: datetime) -> str:
normalized = _normalize_now(value)
return normalized.strftime("%Y-%m-%dT%H:%M:%SZ")
def _normalize_now(value: datetime) -> datetime:
if value.tzinfo is None or value.utcoffset() != timedelta(0):
raise StarHistoryError("clock must return a UTC datetime")
return value.astimezone(UTC).replace(microsecond=0)
def _expect_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None:
actual = set(value)
if actual != expected:
raise StarHistoryError(f"{label} contains missing or unknown fields")
def validate_state(state: Any) -> None:
if not isinstance(state, dict):
raise StarHistoryError("history state must be a JSON object")
_expect_keys(
state,
{
"schema_version",
"repository",
"timezone",View on GitHub (pinned to b5b53acc57)
Solutions
- Use a UTC-aware clock: datetime.now(timezone.utc) or datetime.now(UTC)
- In tests, inject a fixed UTC datetime like datetime(2024, 1, 15, tzinfo=timezone.utc)
- Audit every place the clock is constructed or injected and centralize it
Example fix
# before clock = lambda: datetime.now() # after from datetime import datetime, timezone clock = lambda: datetime.now(timezone.utc)
Defensive patterns
Strategy: type-guard
Validate before calling
from datetime import datetime, timezone
def clock() -> datetime:
now = datetime.now(timezone.utc)
assert now.tzinfo is timezone.utc and now.utcoffset() == timezone.utc.utcoffset(None)
return now Type guard
from datetime import datetime, timedelta, timezone
def is_utc_datetime(value: object) -> TypeGuard[datetime]:
return (
isinstance(value, datetime)
and value.tzinfo is not None
and value.utcoffset() == timedelta(0)
) Try / catch
try:
stamp = _format_state_timestamp(clock())
except StarHistoryError as exc:
if "clock must return a UTC datetime" in str(exc):
raise RuntimeError("fix the injected clock: use datetime.now(timezone.utc)") from exc
raise Prevention
- Standardize on datetime.now(timezone.utc); avoid naive utcnow()
- Inject one shared clock in tests and assert it is tz-aware UTC before use
When it happens
Trigger: A clock function returning datetime.utcnow() (naive, deprecated but common), datetime.now() without tz, or datetime.now(ZoneInfo('Europe/Berlin')).
Common situations: Calling the script's internals from tests with datetime.now() (naive local time) as the clock; overriding the clock with datetime.now(timezone(timedelta(hours=2))); refactors that changed the clock's return type.
Related errors
- GitHub star timestamp was not UTC
- GitHub returned an invalid star timestamp
- {label} must use YYYY-MM-DDTHH:MM:SSZ
- {label} is not a valid UTC timestamp
- history timezone must be UTC
AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14).
Data as JSON: /api/errors/96cddb7e9e3b23bb.
Report an issue: GitHub.