Merge pull request 'next-release' (#47) from next-release into main
All checks were successful
Build Docker Image / build (push) Successful in 46s

Reviewed-on: #47
This commit is contained in:
cal 2026-02-22 22:33:35 +00:00
commit a80addc742
6 changed files with 522 additions and 254 deletions

View File

@ -59,6 +59,13 @@ class MyCog(commands.Cog):
- **Image**: `manticorum67/major-domo-discordapp` (no dash between discord and app)
- **Health**: Process liveness only (no HTTP endpoint)
- **CI/CD**: Gitea Actions on PR to `main` — builds Docker image, auto-generates CalVer version (`YYYY.MM.BUILD`) on merge
### Release Workflow
1. Create feature/fix branches off `next-release` (e.g., `fix/scorebug-bugs`)
2. When done, merge the branch into `next-release` — this is the staging branch where changes accumulate
3. When ready to release, open a PR from `next-release``main`
4. CI builds Docker image on PR; CalVer tag is created on merge
5. Deploy the new image to production (see `/deploy` skill)
- **Other services on same host**: `sba_db_api`, `sba_postgres`, `sba_redis`, `sba-website-sba-web-1`, `pd_api`
### Logs

View File

@ -8,6 +8,8 @@ Runs on a schedule to increment weeks and process contested transactions.
import asyncio
import random
from datetime import datetime, UTC
from utils.timezone import now_chicago
from typing import Dict, List, Tuple, Set, Optional
from dataclasses import dataclass
@ -325,7 +327,7 @@ class TransactionFreezeTask:
self.logger.warning("Could not get current league state")
return
now = datetime.now(UTC)
now = now_chicago()
self.logger.info(
f"Weekly loop check",
datetime=now.isoformat(),
@ -920,6 +922,7 @@ class TransactionFreezeTask:
is_div_week = current.week in [1, 3, 6, 14, 16, 18]
weekly_str = (
f"**Week**: {current.week}\n"
f"**Season**: {season_str}\n"
f"**Time of Day**: {night_str} / {night_str if is_div_week else day_str} / "
f"{night_str} / {day_str}"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,129 @@
"""
Tests for timezone utility module.
Validates centralized timezone helpers that ensure scheduling logic uses
explicit America/Chicago timezone rather than relying on OS defaults.
"""
import pytest
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from utils.timezone import (
CHICAGO_TZ,
now_utc,
now_chicago,
to_chicago,
to_discord_timestamp,
)
class TestChicagoTZ:
"""Tests for the CHICAGO_TZ constant."""
def test_chicago_tz_is_zoneinfo(self):
"""CHICAGO_TZ should be a ZoneInfo instance for America/Chicago."""
assert isinstance(CHICAGO_TZ, ZoneInfo)
assert str(CHICAGO_TZ) == "America/Chicago"
class TestNowUtc:
"""Tests for now_utc()."""
def test_returns_aware_datetime(self):
"""now_utc() should return a timezone-aware datetime."""
result = now_utc()
assert result.tzinfo is not None
def test_returns_utc(self):
"""now_utc() should return a datetime in UTC."""
result = now_utc()
assert result.tzinfo == timezone.utc
class TestNowChicago:
"""Tests for now_chicago()."""
def test_returns_aware_datetime(self):
"""now_chicago() should return a timezone-aware datetime."""
result = now_chicago()
assert result.tzinfo is not None
def test_returns_chicago_tz(self):
"""now_chicago() should return a datetime in America/Chicago timezone."""
result = now_chicago()
assert result.tzinfo == CHICAGO_TZ
class TestToChicago:
"""Tests for to_chicago()."""
def test_converts_utc_datetime(self):
"""to_chicago() should correctly convert a UTC datetime to Chicago time."""
# 2024-07-15 18:00 UTC = 2024-07-15 13:00 CDT (UTC-5 during summer)
utc_dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
result = to_chicago(utc_dt)
assert result.hour == 13
assert result.tzinfo == CHICAGO_TZ
def test_handles_naive_datetime_assumes_utc(self):
"""to_chicago() should treat naive datetimes as UTC."""
naive_dt = datetime(2024, 7, 15, 18, 0, 0)
result = to_chicago(naive_dt)
# Same as converting from UTC
utc_dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
expected = to_chicago(utc_dt)
assert result.hour == expected.hour
assert result.tzinfo == CHICAGO_TZ
def test_preserves_already_chicago(self):
"""to_chicago() on an already-Chicago datetime should be a no-op."""
chicago_dt = datetime(2024, 7, 15, 13, 0, 0, tzinfo=CHICAGO_TZ)
result = to_chicago(chicago_dt)
assert result.hour == 13
assert result.tzinfo == CHICAGO_TZ
def test_winter_offset(self):
"""to_chicago() should use CST (UTC-6) during winter months."""
# 2024-01-15 18:00 UTC = 2024-01-15 12:00 CST (UTC-6 during winter)
utc_dt = datetime(2024, 1, 15, 18, 0, 0, tzinfo=timezone.utc)
result = to_chicago(utc_dt)
assert result.hour == 12
class TestToDiscordTimestamp:
"""Tests for to_discord_timestamp()."""
def test_default_style(self):
"""to_discord_timestamp() should use 'f' style by default."""
dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
result = to_discord_timestamp(dt)
unix_ts = int(dt.timestamp())
assert result == f"<t:{unix_ts}:f>"
def test_relative_style(self):
"""to_discord_timestamp() with style='R' should produce relative format."""
dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
result = to_discord_timestamp(dt, style="R")
unix_ts = int(dt.timestamp())
assert result == f"<t:{unix_ts}:R>"
def test_all_styles(self):
"""to_discord_timestamp() should support all Discord timestamp styles."""
dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
unix_ts = int(dt.timestamp())
for style in ("R", "f", "F", "t", "T", "d", "D"):
result = to_discord_timestamp(dt, style=style)
assert result == f"<t:{unix_ts}:{style}>"
def test_naive_datetime_assumes_utc(self):
"""to_discord_timestamp() should treat naive datetimes as UTC."""
naive_dt = datetime(2024, 7, 15, 18, 0, 0)
aware_dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
assert to_discord_timestamp(naive_dt) == to_discord_timestamp(aware_dt)
def test_chicago_datetime_same_instant(self):
"""to_discord_timestamp() should produce the same unix timestamp regardless of tz."""
utc_dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
chicago_dt = utc_dt.astimezone(CHICAGO_TZ)
assert to_discord_timestamp(utc_dt) == to_discord_timestamp(chicago_dt)

View File

@ -10,7 +10,7 @@ import json
import logging
import time
import uuid
from datetime import datetime
from datetime import datetime, UTC
from typing import Dict, Any, Optional, Union
# Context variable for request tracking across async calls
@ -32,7 +32,7 @@ class JSONFormatter(logging.Formatter):
"""Format log record as JSON with context information."""
# Base log object
log_obj: dict[str, JSONValue] = {
"timestamp": datetime.now().isoformat() + "Z",
"timestamp": datetime.now(UTC).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),

54
utils/timezone.py Normal file
View File

@ -0,0 +1,54 @@
"""
Timezone Utilities
Centralized timezone handling for the Discord bot. The SBA league operates
in America/Chicago time, but the production container may have ambiguous
timezone config. These helpers ensure scheduling logic uses explicit timezones
rather than relying on the OS default.
- Internal storage/logging: UTC
- Scheduling checks (freeze/thaw): America/Chicago
"""
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# League timezone — all scheduling decisions use this
CHICAGO_TZ = ZoneInfo("America/Chicago")
def now_utc() -> datetime:
"""Return the current time as a timezone-aware UTC datetime."""
return datetime.now(timezone.utc)
def now_chicago() -> datetime:
"""Return the current time as a timezone-aware America/Chicago datetime."""
return datetime.now(CHICAGO_TZ)
def to_chicago(dt: datetime) -> datetime:
"""Convert a datetime to America/Chicago.
If *dt* is naive (no tzinfo), it is assumed to be UTC.
"""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(CHICAGO_TZ)
def to_discord_timestamp(dt: datetime, style: str = "f") -> str:
"""Format a datetime as a Discord dynamic timestamp.
Args:
dt: A datetime (naive datetimes are assumed UTC).
style: Discord timestamp style letter.
R = relative, f = long date/short time, F = long date/time,
t = short time, T = long time, d = short date, D = long date.
Returns:
A string like ``<t:1234567890:f>``.
"""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return f"<t:{int(dt.timestamp())}:{style}>"