666ghj/MiroFish · error · StarHistoryError

{label} is not a valid UTC timestamp

Error message

{label} is not a valid UTC timestamp

What it means

A state timestamp passed the strict regex (digits in the right slots) but datetime.strptime still raised ValueError — i.e. the value is structurally right yet semantically invalid, like '2024-02-30T00:00:00Z' or '2024-01-15T25:61:00Z'. _parse_state_timestamp chains it into StarHistoryError(f'{label} is not a valid UTC timestamp').

Source

Thrown at scripts/star_history.py:323


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


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

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Regenerate the state file instead of hand-editing it
  2. Fix the specific impossible timestamp (check Feb 29 in non-leap years)
  3. Make any external generator use datetime.strftime('%Y-%m-%dT%H:%M:%SZ') so ranges are always valid

Example fix

// before (state file)
"generated_at": "2024-02-30T10:30:00Z"

// after (state file)
"generated_at": "2024-02-29T10:30:00Z"
Defensive patterns

Strategy: validation

Validate before calling

from datetime import datetime

def state_timestamp_semantically_valid(value: str) -> bool:
    try:
        datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
        return True
    except ValueError:
        return False

# run after the regex check — catches 2024-02-30, hour 25, etc.

Type guard

def is_valid_state_timestamp(value: object) -> TypeGuard[str]:
    return (
        isinstance(value, str)
        and bool(STATE_TIMESTAMP_RE.fullmatch(value))
        and state_timestamp_semantically_valid(value)
    )

Try / catch

try:
    validate_state(state)
except StarHistoryError as exc:
    if "not a valid UTC timestamp" in str(exc):
        # regex-passed but impossible date (e.g. Feb 30) — file is corrupt; rebuild
        regenerate_state_file()
    else:
        raise

Prevention

When it happens

Trigger: Hand-edited state file with an impossible date (Feb 30, April 31) or out-of-range hour/minute/second; a generator that zero-pads arbitrary integers without range checks.

Common situations: Manual edits to the state JSON; a buggy external tool that fabricates timestamps by string formatting instead of using datetime.strftime.

Related errors


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