{"record":{"id":"160de9f65e206928","repo":"666ghj/MiroFish","slug":"github-star-timestamp-was-not-utc","errorCode":null,"errorMessage":"GitHub star timestamp was not UTC","messagePattern":"GitHub star timestamp was not UTC","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"error","filePath":"scripts/star_history.py","lineNumber":313,"sourceCode":"            has_next_page=has_next_page,\n            end_cursor=end_cursor,\n            rate_remaining=rate_remaining,\n        )\n\n\ndef _strict_non_negative_int(value: Any, label: str) -> int:\n    if type(value) is not int or value < 0:\n        raise StarHistoryError(f\"{label} must be a non-negative integer\")\n    return value\n\n\ndef _parse_github_timestamp(value: str) -> datetime:\n    try:\n        parsed = datetime.fromisoformat(value.replace(\"Z\", \"+00:00\"))\n    except ValueError as exc:\n        raise StarHistoryError(\"GitHub returned an invalid star timestamp\") from exc\n    if parsed.tzinfo is None or parsed.utcoffset() != timedelta(0):\n        raise StarHistoryError(\"GitHub star timestamp was not UTC\")\n    return parsed.astimezone(UTC)\n\n\ndef _parse_state_timestamp(value: Any, label: str) -> datetime:\n    if not isinstance(value, str) or not STATE_TIMESTAMP_RE.fullmatch(value):\n        raise StarHistoryError(f\"{label} must use YYYY-MM-DDTHH:MM:SSZ\")\n    try:\n        parsed = datetime.strptime(value, \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=UTC)\n    except ValueError as exc:\n        raise StarHistoryError(f\"{label} is not a valid UTC timestamp\") from exc\n    return parsed\n\n\ndef _format_state_timestamp(value: datetime) -> str:\n    normalized = _normalize_now(value)\n    return normalized.strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n\n","sourceCodeStart":295,"sourceCodeEnd":331,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L295-L331","documentation":"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.","triggerScenarios":"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.","commonSituations":"Fixtures written without the Z suffix; a stub emitting local-time timestamps; a gateway that re-serializes dates into local offsets.","solutions":["Emit timestamps ending in 'Z' (UTC) everywhere: '2024-01-15T10:30:00Z'","Convert local times to UTC before serializing in whichever component produced the value"],"exampleFix":"// before\n\"starredAt\": \"2024-01-15T10:30:00+02:00\"\n\n// after\n\"starredAt\": \"2024-01-15T08:30:00Z\"","handlingStrategy":"validation","validationCode":"from datetime import datetime, timezone\n\ndef is_utc_iso(value: str) -> bool:\n    try:\n        dt = datetime.fromisoformat(value.replace(\"Z\", \"+00:00\"))\n    except ValueError:\n        return False\n    return dt.tzinfo is not None and dt.utcoffset() == timezone.utc.utcoffset(None)","typeGuard":"def is_github_utc_timestamp(value: object) -> TypeGuard[str]:\n    return (\n        isinstance(value, str)\n        and value.endswith(\"Z\")\n        and is_utc_iso(value)\n    )","tryCatchPattern":"try:\n    ts = _parse_github_timestamp(starred_at)\nexcept StarHistoryError as exc:\n    if \"not UTC\" in str(exc):\n        raise StarHistoryError(f\"convert {starred_at!r} to UTC ('...Z') at the source\") from exc\n    raise","preventionTips":["Emit only 'Z'-suffixed timestamps from stubs and fixtures","Convert local times to UTC before serializing; never rely on the parser to guess zones"],"tags":["datetime","utc","validation","iso8601","star-history"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}