Skip to content

fix(server): route weekly leaderboard Slack notifications per workspace #1246

@FelixTJDietrich

Description

@FelixTJDietrich

Part of #1202. Closes #1050. Supersedes #771.

What ships

SlackWeeklyLeaderboardTask migrates from the global LeaderboardProperties.workspaceSlugs + global Slack channel to per-workspace routing via the workspace integration registry. The task iterates active workspaces; for each workspace it resolves the Slack App via SlackAppFactory.appFor(workspaceId) and the channel + team via the workspace's workspace_integration config payload. Workspaces without a Slack integration are skipped with a structured log. The global allowlist is removed.

The per-workspace leaderboard_notification_* columns on Workspace (leaderboard_notification_enabled, leaderboard_notification_team, leaderboard_notification_channel_id) are read during the dual-read window into the workspace_integration.config_jsonb payload during the registry migration that the workspace integration registry epic owns. This sub-issue is the consumer; it does not change the migration shape, but it does flip SlackWeeklyLeaderboardTask from reading Workspace.leaderboard_notification_* to reading via the registry-backed accessor.

The companion SlackMessageService (currently bound to the global token) is updated to take a workspaceId and resolve App per call.

Why

The bug observed in #1050 is that one global Slack channel receives N messages per leaderboard cycle (one per active workspace), all from the global token. PR #771 attempted a half-migration directly against the denormalized Workspace columns; that effort stalled and is superseded by routing through the registry from the start. The user-visible fix is that each workspace's leaderboard lands in that workspace's configured channel from that workspace's bot.

This sub-issue is also the migration that gets us off the leaderboard-task–singleton dependency that defined the legacy global path; with this in, no production caller resolves a Slack App from LeaderboardProperties.

Acceptance criteria

  • SlackWeeklyLeaderboardTask.run() iterates active workspaces only; per iteration it calls SlackAppFactory.appFor(workspaceId) and reads channel + team from the registry config payload
  • Workspaces with workspace_integration of kind SLACK and a leaderboard_notification_enabled=true flag post their leaderboard to their configured channel; all others are skipped with INFO logs carrying workspace_id MDC
  • LeaderboardProperties.workspaceSlugs is removed; no production code reads it
  • SlackMessageService.post(workspaceId, channelId, blocks) is the only public surface; no method signature takes the global token implicitly
  • An integration test installs the Slack app for two workspaces (different channels, different tokens) and asserts both receive exactly one leaderboard message after one task tick — to two different channels using two different App instances
  • A test asserts a workspace without a Slack integration is skipped silently (no exception, no message)
  • Leaderboard Slack notifications fan out to all workspaces on a single global channel #1050 is closed referencing this issue; the GitHub link in PR fix(leaderboard): use workspace-specific Slack notification settings #771's comment trail is updated to point here

Tests to write

Implementation notes

  • The leaderboard_notification_* Workspace columns are still being read during the registry-migration dual-read window; this sub-issue uses the registry-backed accessor that the workspace integration registry epic exposes. When the column-drop sub-issue (chore(server): drop denormalized workspace.slack_* columns #1249) lands, this code path is already correct.
  • SlackMessageService was the singleton-bound class today; the migration here moves it onto the factory and removes its @Value("${hephaestus.leaderboard.notification.slack-token}") field. That field becomes dead in this sub-issue and is deleted; the property is removed from application.yml in the same PR.
  • The leaderboard transformation epic (a separate, larger epic outside this slice) deletes SlackWeeklyLeaderboardTask entirely as part of the ELO + league-points removal. Sequencing: this sub-issue ships before that deletion to close Leaderboard Slack notifications fan out to all workspaces on a single global channel #1050; the deletion supersedes this work later. The cost of doing both is small because the deletion is a straight remove, and the per-workspace routing fix is a hard prerequisite to having a deletable test fixture (you can't assert "deleting the task takes no Slack posts off the air for any workspace" if the task only ever wrote to one global channel).

Dependencies

Depends on #1244, #1216. Closes #1050. Supersedes #771.

Metadata

Metadata

Assignees

No one assigned

    Labels

    application-serverSpring Boot server: APIs, business logic, databasebugSomething isn't workingpriority:highAddress this sprint - Significant impact

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions