Auto-generates next CalVer tag (YYYY.M.BUILD) or accepts explicit
version. Shows commits since last tag, confirms, then pushes tag
to trigger CI build.
Usage:
.scripts/release.sh # auto-generate next version
.scripts/release.sh 2026.3.11 # explicit version
.scripts/release.sh -y # skip confirmation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use SSH alias (ssh akamai) instead of manual ssh -i command
- Change image tag from :latest to :production
- Fix rollback command to use SSH alias
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace branch-push trigger with tag-push trigger (20* pattern).
Version is extracted from the git tag itself instead of auto-generated.
Docker images are tagged with the CalVer version + floating "production" tag.
To release: git tag YYYY.M.BUILD && git push --tags
Also updates CLAUDE.md to document the new workflow and removes all
next-release branch references (branch retired).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes#90
Replace sequential awaits with asyncio.gather() in all locations identified
in the issue:
- commands/gameplay/scorebug.py: parallel team lookups in publish_scorecard
and scorebug commands; also fix missing await on async scorecard_tracker calls
- commands/league/submit_scorecard.py: parallel away/home team lookups
- tasks/live_scorebug_tracker.py: parallel team lookups inside per-scorecard
loop (compounds across multiple active games); fix missing await on
get_all_scorecards
- commands/injuries/management.py: parallel get_current_state() +
search_players() in injury_roll, injury_set_new, and injury_clear
- services/trade_builder.py: parallel per-participant roster validation in
validate_trade()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move inspect.signature(func) calls from inside wrapper functions to the
outer decorator function so the introspection cost is paid once at
decoration time instead of on every invocation.
- logged_command: sig, param_names, and exclude_set computed at decoration time;
wrapper.__signature__ reuses the pre-computed sig
- cached_api_call: sig moved to decorator scope; bound_args still computed
per-call (requires runtime args)
- cached_single_item: same as cached_api_call
Closes#97
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The PR trigger caused Docker builds on every push to PR branches,
wasting CI resources. Only build on merge to main/next-release.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes#96
Replaces the per-field `json.dumps(value)` probe — which fully serialized
and discarded the result just to check serializability — with a type-check
fast path using `isinstance()`. The `_SERIALIZABLE_TYPES` tuple is defined
at module level so it's not recreated on every log call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#104
- Remove duplicate `self.maintenance_mode: bool = False` assignment (merge
artifact from PR #83)
- Delete dead `@self.tree.interaction_check` block in `setup_hook` that
generated a RuntimeWarning at startup; `MaintenanceAwareTree.interaction_check()`
already handles this correctly via the `tree_cls=MaintenanceAwareTree` kwarg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The parameter was already ignored (body hardcodes range(1, 19)).
Remove from signature and the one caller that passed it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace custom _make_game/_make_team helpers with existing test
factories for consistency with the rest of the test suite.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes#88
Replaced sequential for-loops in get_team_schedule(), get_recent_games(),
and get_upcoming_games() with asyncio.gather() to fire all per-week HTTP
requests concurrently. Also adds import asyncio which was missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#98
Replace blocking `client.keys(pattern)` with non-blocking
`client.scan_iter(match=pattern)` to avoid full-keyspace scans
that block the Redis server during cache invalidation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#99
- Add module-level `_user_team_cache` dict with 60-second TTL so
`get_user_major_league_team` is called at most once per minute per
user instead of on every keystroke.
- Reduce `search_players(limit=50)` to `limit=25` to match Discord's
25-choice display cap and avoid fetching unused results.
- Add `TestGetCachedUserTeam` class covering cache hit, TTL expiry, and
None caching; add `clear_user_team_cache` autouse fixture to prevent
test interference via module-level state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The data/ volume was mounted :ro to protect Google Sheets credentials,
but this also prevented all state trackers from persisting JSON files
(scorecards, voice channels, trade channels, soak data), causing silent
save failures and stale data accumulating across restarts.
- Mount only the credentials file as :ro (file-level mount)
- Add a separate :rw storage/ volume for runtime state files
- Move all tracker default paths from data/ to storage/
- Add STATE_HOST_PATH env var (defaults to ./storage)
- Update SHEETS_CREDENTIALS_HOST_PATH semantics: now a file path
(e.g. ./data/major-domo-service-creds.json) instead of a directory
- Add storage/ to .gitignore
Closes#85
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep both the type: ignore annotation and the logger.info call
in admin_maintenance.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous code attempted to register a maintenance mode gate via
@self.tree.interaction_check inside setup_hook. That pattern is invalid
in discord.py — interaction_check is an overridable method on CommandTree,
not a decorator. The assignment was silently dropped, making maintenance
mode a no-op and producing a RuntimeWarning about an unawaited coroutine.
Changes:
- Add MaintenanceAwareTree(discord.app_commands.CommandTree) that overrides
interaction_check: blocks non-admins when bot.maintenance_mode is True,
always passes admins through, no-op when maintenance mode is off
- Pass tree_cls=MaintenanceAwareTree to super().__init__() in SBABot.__init__
- Add self.maintenance_mode: bool = False to SBABot.__init__
- Update /admin-maintenance command to actually toggle bot.maintenance_mode
- Add tests/test_bot_maintenance_tree.py with 8 unit tests covering all
maintenance mode states, admin pass-through, DM context, and missing attr
Closes#82
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read all spreadsheet data (plays, box score, pitching decisions) before any
database writes so formula errors like #N/A don't leave the DB in a partial
state. Also preserve SheetsException detail through the error chain and show
users the specific cell/error instead of a generic failure message.
Closes#78
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pin redis==7.3.0 and move to requirements.txt (production)
- Create requirements-dev.txt with all dev/test deps pinned to exact versions
(pytest-mock==3.15.1, black==26.1.0, ruff==0.15.0)
- Remove dev/test tools from requirements.txt (not needed in Docker image)
- Document pinning policy and requirements-dev.txt usage in CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ensures all client.post() calls to collection endpoints include
trailing slashes, matching the standardized database API routes.
Covers BaseService.create(), TransactionService, InjuryService,
and DraftListService POST calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reverts universal trailing slash in _build_url which broke custom_commands
endpoints (401 on /execute/). Instead, add trailing slashes only to the
two batch POST endpoints (plays/, decisions/) that need them to avoid
307 redirects dropping request bodies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The FastAPI server returns 307 redirects for URLs without trailing slashes.
aiohttp follows these redirects but converts POST to GET, silently dropping
the request body. This caused play-by-play and decision data from
/submit-scorecard to never be persisted to the database despite the API
returning success.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add centralized `is_admin(interaction)` helper that includes the
`isinstance(interaction.user, discord.Member)` guard, preventing
AttributeError in DM contexts.
Use it in `can_edit_player_image()` which previously accessed
`guild_permissions.administrator` directly without the isinstance
guard. Update the corresponding test to mock the user with
`spec=discord.Member` so the isinstance check passes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>