666ghj/MiroFish · error · StarHistoryError

GitHub star timestamp was not UTC

Error message

GitHub star timestamp was not UTC

What it means

The starredAt string parsed successfully as ISO 8601 but the resulting datetime was naive (no tzinfo) or its UTC offset was not zero. The script's whole model assumes UTC instants, so _parse_github_timestamp raises StarHistoryError('GitHub star timestamp was not UTC') instead of guessing a timezone. Note the rewrite only converts a trailing 'Z'; offsets like '+02:00' survive parsing and land here.

Source

Thrown at scripts/star_history.py:313

            has_next_page=has_next_page,
            end_cursor=end_cursor,
            rate_remaining=rate_remaining,
        )


def _strict_non_negative_int(value: Any, label: str) -> int:
    if type(value) is not int or value < 0:
        raise StarHistoryError(f"{label} must be a non-negative integer")
    return value


def _parse_github_timestamp(value: str) -> datetime:
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError as exc:
        raise StarHistoryError("GitHub returned an invalid star timestamp") from exc
    if parsed.tzinfo is None or parsed.utcoffset() != timedelta(0):
        raise StarHistoryError("GitHub star timestamp was not UTC")
    return parsed.astimezone(UTC)


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")

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Emit timestamps ending in 'Z' (UTC) everywhere: '2024-01-15T10:30:00Z'
  2. Convert local times to UTC before serializing in whichever component produced the value

Example fix

// before
"starredAt": "2024-01-15T10:30:00+02:00"

// after
"starredAt": "2024-01-15T08:30:00Z"
Defensive patterns

Strategy: validation

Validate before calling

from datetime import datetime, timezone

def is_utc_iso(value: str) -> bool:
    try:
        dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return False
    return dt.tzinfo is not None and dt.utcoffset() == timezone.utc.utcoffset(None)

Type guard

def is_github_utc_timestamp(value: object) -> TypeGuard[str]:
    return (
        isinstance(value, str)
        and value.endswith("Z")
        and is_utc_iso(value)
    )

Try / catch

try:
    ts = _parse_github_timestamp(starred_at)
except StarHistoryError as exc:
    if "not UTC" in str(exc):
        raise StarHistoryError(f"convert {starred_at!r} to UTC ('...Z') at the source") from exc
    raise

Prevention

When it happens

Trigger: starredAt like '2024-01-15T10:30:00' (no zone at all) or '2024-01-15T10:30:00+02:00' — both parse via fromisoformat but fail the offset check. Real GitHub always sends 'Z'-suffixed UTC, so this almost always comes from fixtures or a transforming proxy.

Common situations: Fixtures written without the Z suffix; a stub emitting local-time timestamps; a gateway that re-serializes dates into local offsets.

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/160de9f65e206928. Report an issue: GitHub.