You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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)
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).
Part of #1202. Closes #1050. Supersedes #771.
What ships
SlackWeeklyLeaderboardTaskmigrates from the globalLeaderboardProperties.workspaceSlugs+ global Slack channel to per-workspace routing via the workspace integration registry. The task iterates active workspaces; for each workspace it resolves the SlackAppviaSlackAppFactory.appFor(workspaceId)and the channel + team via the workspace'sworkspace_integrationconfig payload. Workspaces without a Slack integration are skipped with a structured log. The global allowlist is removed.The per-workspace
leaderboard_notification_*columns onWorkspace(leaderboard_notification_enabled,leaderboard_notification_team,leaderboard_notification_channel_id) are read during the dual-read window into theworkspace_integration.config_jsonbpayload 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 flipSlackWeeklyLeaderboardTaskfrom readingWorkspace.leaderboard_notification_*to reading via the registry-backed accessor.The companion
SlackMessageService(currently bound to the global token) is updated to take aworkspaceIdand resolveAppper 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
AppfromLeaderboardProperties.Acceptance criteria
SlackWeeklyLeaderboardTask.run()iterates active workspaces only; per iteration it callsSlackAppFactory.appFor(workspaceId)and reads channel + team from the registry config payloadworkspace_integrationof kindSLACKand aleaderboard_notification_enabled=trueflag post their leaderboard to their configured channel; all others are skipped withINFOlogs carryingworkspace_idMDCLeaderboardProperties.workspaceSlugsis removed; no production code reads itSlackMessageService.post(workspaceId, channelId, blocks)is the only public surface; no method signature takes the global token implicitlyAppinstancesTests to write
SlackWeeklyLeaderboardTaskIT— two-workspace fixture, one tick, asserts per-workspace routing.Implementation notes
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.SlackMessageServicewas 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 fromapplication.ymlin the same PR.SlackWeeklyLeaderboardTaskentirely 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.