From 29f9ecf9714fcfea4bba4863c1c77e2e73f35e47 Mon Sep 17 00:00:00 2001 From: Amit kumar Date: Tue, 28 Jul 2026 03:00:44 +0530 Subject: [PATCH 1/4] feat(integrations): build slack notification pipeline --- SLACK_INTEGRATION_PLAN.md | 362 ++++++++++++++++++ SLACK_INTEGRATION_PRD.md | 246 ++++++++++++ Task.md | 131 +++++++ Todo.md | 6 + apps/api/internal/handler/instance.go | 42 +- apps/web/package.json | 1 + apps/web/src/api/types.ts | 9 + .../components/layout/InstanceAdminLayout.tsx | 1 + .../InstanceAdminIntegrationSlackPage.tsx | 305 +++++++++++++++ .../InstanceAdminIntegrationsPage.tsx | 46 ++- apps/web/src/pages/instance-admin/index.ts | 1 + apps/web/src/routes/index.tsx | 14 + instance_settings_202607251917.json | 45 +++ smee.log | 12 + 14 files changed, 1216 insertions(+), 5 deletions(-) create mode 100644 SLACK_INTEGRATION_PLAN.md create mode 100644 SLACK_INTEGRATION_PRD.md create mode 100644 Task.md create mode 100644 Todo.md create mode 100644 apps/web/src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx create mode 100644 instance_settings_202607251917.json create mode 100644 smee.log diff --git a/SLACK_INTEGRATION_PLAN.md b/SLACK_INTEGRATION_PLAN.md new file mode 100644 index 00000000..eba817fc --- /dev/null +++ b/SLACK_INTEGRATION_PLAN.md @@ -0,0 +1,362 @@ +# Slack Integration — Implementation Plan + +Delivering Devlane notifications to Slack channels. This plan follows the task +spec in [`Taks.md`](./Taks.md) and mirrors two existing, fully-wired patterns in +the codebase: + +- **GitHub integration** → the *integration scaffolding* (OAuth, install flow, + provider package, `integrations` / `workspace_integrations` storage, layered + `handler → service → store`, workspace-scoped nested routes with trailing + slashes, instance-settings credential resolution). +- **Email notifications** → the *delivery mechanism* (fan-out in + `service/notification.go`, background `queue.Publisher` → RabbitMQ → + `queue.Consumer` with the built-in 3-retry machinery, graceful degradation + when RabbitMQ is absent). + +The result fuses the two: Slack install/config looks like GitHub; Slack message +posting rides the same queue path as email. + +--- + +## 1. Scope (v1) + +**In scope** +- Register `slack` as an integration provider and let a workspace connect it via + Slack OAuth. +- Store the bot token + team info per workspace install; link a **Slack channel + per project**. +- On notification events (assigned / state changed / commented / mentioned / + field changed), post a formatted message to the linked channel via + `chat.postMessage`, routed through the background queue. +- Instance-admin UI to store Slack app credentials in `instance_settings`. +- Web UI: Slack provider card (Connect/Disconnect) + per-project channel + select/unlink. + +**Out of scope (v1)** — matches `Taks.md` +- Per-user DMs / mapping Devlane users ↔ Slack users (channel-level only). +- Two-way sync, slash commands, interactive actions. +- Threaded conversation mirroring. + +> Note on behavior: v1 posts to a **shared project channel**, not a DM to the +> assignee. The email/in-app notifications remain the per-user path. + +--- + +## 2. Architecture at a glance + +``` +Issue mutation (assign / state / comment / field change) + → IssueService / CommentService + → NotificationService.Issue* (service/notification.go) + → emit() ── in-app rows (unchanged) + ├─ enqueueNotificationEmails → queue.PublishSendEmail (existing) + └─ enqueueSlackNotifications → queue.PublishSlackPost (NEW) + │ + RabbitMQ "devlane.slack" queue (NEW) + │ + queue.Consumer → HandleSlackPost (NEW) + │ + slack.PostMessage → chat.postMessage (NEW) +``` + +Credential/config resolution mirrors GitHub: Slack **app** creds (client id / +secret / signing secret) live in `instance_settings` under a new `slack` key; +the per-workspace **bot token** lives on the `workspace_integrations` row; the +per-project **channel** lives in a new table. + +--- + +## 3. Backend + +### 3.1 Data model + migration + +**New migration:** `apps/api/migrations/000007_slack_integration.{up,down}.sql` +(next free number after `000006_instance_admins`). Never edit merged migrations; +ship both up and down. + +- **Seed the provider row** into `integrations` (mirrors how `github` is + registered): + ```sql + INSERT INTO integrations (title, provider, network, verified) + VALUES ('Slack', 'slack', 1, true) + ON CONFLICT (provider) DO NOTHING; + ``` +- **Reuse `workspace_integrations`** for the install. Store on the existing row: + - `account_login` → Slack team/workspace name + - `metadata` (jsonb) → `{ "team_id": "T…", "bot_user_id": "U…", "scope": "…" }` + - `config` (jsonb) → bot access token, encrypted. Follow the + `WebhookSecret` / `Credentials` `json:"-"` rule so the token is **never** + serialized to the client. +- **New table `slack_channel_links`** (per project ↔ channel), mirroring + `github_repository_syncs`: + ```sql + CREATE TABLE slack_channel_links ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_integration_id UUID NOT NULL REFERENCES workspace_integrations (id) ON DELETE CASCADE, + project_id UUID NOT NULL REFERENCES projects (id) ON DELETE CASCADE, + workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE, + channel_id VARCHAR(64) NOT NULL, -- Slack "C…" id + channel_name VARCHAR(255) NOT NULL, + events JSONB NOT NULL DEFAULT '{}', -- per-event enable flags + actor_id UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ, + created_by_id UUID, + updated_by_id UUID, + UNIQUE (project_id, deleted_at) -- one active channel link per project + ); + ``` + +**New model file:** `apps/api/internal/model/slack.go` +- `type SlackChannelLink struct { … }` with `TableName() "slack_channel_links"` + and a `BeforeCreate` UUID hook — copy the shape from + `model/integration.go`'s `GithubRepositorySync`. Keep the token field + (if any is duplicated here) `json:"-"`. + +### 3.2 Instance settings (Slack app credentials) + +**`apps/api/internal/handler/instance.go`** +- Add `slack` to `allowedSettingKeys` (line ~25) and to the section list in + `GetSettings` (line ~272). +- Add a `defaultSettingValue("slack")` case: + ```go + case "slack": + return model.JSONMap{"client_id": "", "client_secret_set": false, "signing_secret_set": false} + ``` +- Register encrypted fields in `secretKeysBySection` (line ~281): + ```go + "slack": {"client_secret", "signing_secret"}, + ``` +- Add a merge/encrypt branch in `UpdateSetting` for `key == "slack"` that + encrypts `client_secret` / `signing_secret` via `crypto.EncryptOrPlain` and + sets the `*_set` booleans (copy the `github_app` / `email` branch pattern). + +### 3.3 Slack OAuth provider + +**New file:** `apps/api/internal/oauth/slack.go` — implement the same interface +as `oauth/github.go` (`Name`, `AuthURL`, `Exchange`, `GetUserInfo` where +relevant). For Slack use the **v2** OAuth endpoints: +- Authorize: `https://slack.com/oauth/v2/authorize` +- Token exchange: `https://slack.com/api/oauth.v2.access` +- Bot scopes: `chat:write`, `channels:read`, `groups:read` (and + `channels:join` if auto-joining public channels). +- The token response returns `access_token` (bot token, `xoxb-…`), `team.id`, + `team.name`, `bot_user_id`, `scope` — capture these into `TokenData` + + workspace-integration metadata. + +> Slack's OAuth is workspace-install-oriented (not "login as user"), so unlike +> the GitHub *login* provider this is only used for the **install** flow, not +> auth. Keep the provider minimal. + +### 3.4 Slack client package + +**New package:** `apps/api/internal/slack/` (mirrors `internal/github/`) +- `client.go` — thin HTTP client for the Slack Web API: + - `PostMessage(ctx, token, channelID, text string, blocks any) error` → + `POST https://slack.com/api/chat.postMessage`. Slack returns HTTP 200 with a + JSON `{ "ok": false, "error": "…" }` on logical failures — **must** check + the `ok` field and return an error so the queue retry logic engages. + - `ListChannels(ctx, token string, cursor string) ([]Channel, nextCursor, error)` + → `conversations.list` (for the channel-select UI). +- `verify.go` — `VerifySignature(signingSecret, timestamp, body, header)` using + Slack's `v0=` HMAC-SHA256 scheme (mirror `github/webhook.go`'s + `VerifySignature`). Only needed if/when inbound events are added; include the + stub now for parity but it's optional for v1 (no inbound events in scope). +- `notification.go` — `BuildSlackMessage(sender string, data …) (text, blocks)` + mirroring `mail/notification.go`'s `BuildNotificationEmail`, producing Slack + Block Kit blocks (issue ref, title, actor, before/after, link). + +### 3.5 Store layer + +**New file:** `apps/api/internal/store/slack.go` — `SlackChannelLinkStore` +(pure DB, no service calls): +- `Create`, `GetByProject`, `Update`, `SoftDelete`, `ListByWorkspaceIntegration`. +- Copy the structure of `store/*` github stores. + +### 3.6 Service layer + +**New file:** `apps/api/internal/service/slack.go` — `SlackService` +(business logic; enforces workspace/project membership like `IntegrationService` +and `GithubSyncService`): +- `InstallSlack(ctx, workspaceSlug, userID, tokenData)` — creates/updates the + `workspace_integrations` row for provider `slack`, stores encrypted bot token. +- `Uninstall` path — reuse `IntegrationService.Uninstall` (already generic on + `:provider`), and **cascade**: soft-delete all `slack_channel_links` for the + workspace so delivery stops (acceptance criterion). +- `ListChannels(ctx, workspaceSlug, userID)` — proxies `slack.ListChannels` + using the stored bot token. +- `LinkChannel` / `GetChannelForProject` / `UnlinkChannel` — per-project channel + management. +- `LoadSlackAppCredsFromSettings(ctx, settings)` helper — mirrors + `service.LoadGitHubAppNameFromSettings` / `LoadGitHubWebhookSecretFromSettings`. +- Define sentinel errors (`ErrSlackNotConfigured`, `ErrSlackNotInstalled`, + `ErrChannelLinkNotFound`, …) and map them in `writeIntegrationError`. + +### 3.7 Notification fan-out hook (the delivery leg) + +**`apps/api/internal/queue/queue.go`** +- Add queue + task constants: + ```go + QueueSlack = "devlane.slack" + TaskSlackPost = "slack_post" + ``` +- Declare `QueueSlack` in `NewPublisher` (add to the queue slice + `queues` map). +- Add `type SlackPostPayload struct { WorkspaceIntegrationID, ChannelID, Text string; Blocks any; Kind string }` + and `PublishSlackPost(ctx, payload)` (mirror `PublishSendEmail`). + +**`apps/api/internal/queue/consumer.go`** +- Add `HandleSlackPost(log, poster func(ctx, token, channelID, text, blocks) error) TaskHandler` + mirroring `HandleSendEmail` (decode → resolve token → post → return err so the + existing `maxRetries = 3` republish logic applies). + +**`apps/api/internal/service/notification.go`** +- Add `s.slackQueue`/config wiring + `SetSlack…` setters next to the email ones. +- In `emit()` (after `enqueueNotificationEmails`, ~line 332), add: + ```go + if s.slackEnabled() { + s.enqueueSlackNotifications(ctx, allowed, params, actorName, issueRef) + } + ``` + Gate it exactly like email (`queue != nil && appURL != ""`) so a Slack outage + or missing RabbitMQ never blocks the user's action. +- Implement `enqueueSlackNotifications`: resolve the issue's **project → + channel link**; if the project has a linked channel and the event type is + enabled in `events`, build the message and `PublishSlackPost`. Note this is + **channel-scoped** (one post per project channel), not per-receiver — so it + runs once per issue event, not once per receiver like email. + +> Because delivery is channel-based, the natural trigger key is the issue's +> project, not the receiver set. Consider posting whenever the event fires and a +> channel is linked (independent of who the in-app receivers are), which better +> matches "team channel" semantics. Confirm this product choice before building. + +### 3.8 Wiring & routes + +**`apps/api/cmd/api/main.go`** +- Build the Slack poster (`slack.NewClient`) and register the consumer handler: + ```go + consumer.Register(queue.QueueSlack, queue.HandleSlackPost(log, slackPoster)) + consumer.Run(ctx, []string{queue.QueueEmails, queue.QueueWebhooks, queue.QueueSlack}) + ``` + +**`apps/api/internal/router/router.go`** +- Construct `slackSvc := service.NewSlackService(...)`, add it to + `IntegrationHandler` (extend the struct), wire the queue into + `notificationSvc` (a `SetSlackQueue` alongside `SetQueue`). +- Add a hot-reload hook for the `slack` settings key (mirror the `github_app` + reload at router.go ~line 219). +- Register routes (mirror the GitHub block, trailing slashes intentional): + ``` + GET /auth/slack/install?workspace=:slug (RequireAuth) → SlackInstallStart + GET /auth/slack/callback (RequireAuth) → SlackInstallCallback + GET /api/workspaces/:slug/integrations/slack/channels/ → SlackListChannels + GET /api/workspaces/:slug/projects/:projectId/integrations/slack/channel/ → SlackGetChannel + POST /api/workspaces/:slug/projects/:projectId/integrations/slack/channel/ → SlackLinkChannel + PATCH /api/workspaces/:slug/projects/:projectId/integrations/slack/channel/ → SlackUpdateChannel (event toggles) + DELETE /api/workspaces/:slug/projects/:projectId/integrations/slack/channel/ → SlackUnlinkChannel + ``` + Uninstall reuses the existing generic + `DELETE /api/workspaces/:slug/integrations/:provider/`. + +**`apps/api/internal/handler/integration.go`** (or a new `integration_slack.go`) +- Add `SlackInstallStart` / `SlackInstallCallback` mirroring + `GitHubInstallStart` / `GitHubInstallCallback`: carry workspace slug in a + state cookie, redirect back to + `//settings?section=integrations` with `?connected=slack` / `?error=…` + (reuse the `redirectIntegration` helper, generalizing the hardcoded + `connected=github`). + +--- + +## 4. Frontend (`apps/web`) + +### 4.1 Service + types +- **`src/services/integrationService.ts`** — add: + - `slackInstallUrl(workspaceSlug)` (top-level navigation, like + `githubInstallUrl`). + - `slackListChannels(workspaceSlug)` + - `slackGetProjectChannel` / `slackLinkProjectChannel` / + `slackUpdateProjectChannel` / `slackUnlinkProjectChannel` + (404 → null pattern, as `githubGetProjectSync` does). +- **`src/api/types.ts`** — add `SlackChannel`, `SlackChannelLinkResponse`, and + extend the integration provider union to include `slack`. + +### 4.2 Integrations UI +- **`src/components/integrations/IntegrationsSection.tsx`** — currently + hardcoded to `installed.find((wi) => wi.provider === 'github')`. Generalize to + render a list/registry of providers and add a **Slack card** (Connect / + Disconnect + "Connected as "). +- **New `src/components/integrations/SlackChannelSettingsModal.tsx`** — per + project: list channels the bot can post to, link/unlink, and event toggles. + Model it on `RepoSyncSettingsModal.tsx`. + +### 4.3 Instance admin +- **New `src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx`** — + form for Slack app `client_id`, `client_secret`, `signing_secret` (write-only + secret pattern with `*_set` masks, exactly like `InstanceAdminEmailPage` / + the GitHub App page). +- Register it in `src/pages/instance-admin/index.ts` and add the lazy route + + nav entry in `src/routes/index.tsx` (mirror + `InstanceAdminIntegrationGitHubPage`, route + `instance-admin/integrations/slack`). + +--- + +## 5. Testing & validation + +- **Go unit tests** (`go test ./...`): `slack` signature verify (if included), + `BuildSlackMessage`, service membership enforcement, channel link CRUD. Mirror + `github/refparse_test.go` and `service` test patterns. +- **Queue retry**: reuse existing `consumer` behavior; add a test asserting a + failing `chat.postMessage` republishes with incremented `x-retry-count` and + discards after 3. +- **Local manual test**: connect Slack in a dev workspace, link a channel, then + assign/comment on an issue and confirm the message posts. (For a fully offline + loop, a stub Slack endpoint can stand in for `chat.postMessage`, analogous to + the Mailpit setup used for email.) +- **`npm run validate`** must pass (web typecheck + lint + prettier + go vet + + go test). +- Conventional Commits; branch + PR (don't commit to `main`); disclose AI + assistance per `CONTRIBUTING.md`. + +--- + +## 6. Acceptance criteria (from `Taks.md`) → where satisfied + +| Criterion | Satisfied by | +| --- | --- | +| Admin enters Slack app creds in instance settings | §3.2 + §4.3 | +| Workspace admin connects Slack via OAuth, sees "Connected" | §3.3 + §3.8 + §4.2 | +| Link/unlink a Slack channel per project | §3.5–3.6 + §4.2 | +| Configured events post a formatted message to the channel | §3.4 + §3.7 | +| Slack tokens never returned to client | §3.1 (`json:"-"` on token fields) | +| Slack/RabbitMQ failure never blocks the originating action | §3.7 (queue path + gating + graceful degrade) | +| Disconnecting Slack unlinks channels & stops delivery | §3.6 (cascade soft-delete) | +| Layered arch, workspace-scoped URLs w/ trailing slashes, up+down migrations, `npm run validate` | §3.8 + §3.1 + §5 | + +--- + +## 7. Suggested delivery order (PR-sized steps) + +1. **Migration + model + store** (`000007`, `model/slack.go`, `store/slack.go`) — no behavior yet. +2. **Instance settings `slack` key** (backend + admin UI) — creds can be saved. +3. **OAuth provider + install/callback handler + routes** — Connect/Disconnect works end-to-end; token stored. +4. **Channel list + per-project link/unlink** (service + routes + web modal). +5. **Queue task + consumer + `slack.PostMessage` + notification hook** — messages actually post. +6. **Polish**: event toggles, error states, tests, `npm run validate`. + +Each step is independently reviewable and leaves the app in a working state. + +--- + +## 8. Open questions to confirm before building + +1. **Trigger semantics**: post to the channel on every configured event + regardless of in-app receivers (team-channel model), or only when there's at + least one in-app receiver? (§3.7) +2. **Which events** are channel-worthy by default (all five, or a subset like + created/state-changed)? Drives the `events` jsonb defaults. +3. **Public vs private channels**: require the bot to be invited, or attempt + `channels.join` for public channels automatically? (affects OAuth scopes) +4. **Message format**: plain text vs Block Kit richness for v1. diff --git a/SLACK_INTEGRATION_PRD.md b/SLACK_INTEGRATION_PRD.md new file mode 100644 index 00000000..9b8563a9 --- /dev/null +++ b/SLACK_INTEGRATION_PRD.md @@ -0,0 +1,246 @@ +# PRD — Slack Integration for Channel Notifications + +| Field | Value | +| --- | --- | +| **Product** | Devlane | +| **Feature** | Slack Integration (channel notifications) | +| **Type** | Feature | +| **Priority** | Medium | +| **Area** | Integrations (API + Web) | +| **Status** | Draft | +| **Author** | — | +| **Related docs** | [`Taks.md`](./Taks.md) (task spec), [`SLACK_INTEGRATION_PLAN.md`](./SLACK_INTEGRATION_PLAN.md) (implementation plan) | + +--- + +## 1. Overview + +Devlane's issue activity (assignments, state changes, comments, mentions, field +changes) currently flows only to **in-app notifications** and **email**. Teams +coordinate in Slack, but there is no supported way to surface that activity in a +Slack channel. This feature lets a workspace connect Slack and post configured +issue events to a **project-linked channel**, so a team sees relevant updates +where they already work. + +The integration reuses Devlane's existing patterns: the **GitHub integration** +for connect/config scaffolding, and the **email notification pipeline** (async +queue with retries) for delivery. + +--- + +## 2. Problem statement + +- Activity that matters to a team (e.g. "issue moved to In Review", "someone + commented") is invisible in Slack, where the team actually coordinates. +- Users who want this today must build their own polling or webhook tooling. +- This is a gap relative to comparable tools and relative to Devlane's own + GitHub integration, which is fully productionized while Slack "exists in name + only" (a doc comment referencing `slack` with no model, OAuth, package, or UI). + +--- + +## 3. Goals & non-goals + +### Goals +- G1. A workspace admin can connect Slack to a workspace via OAuth in a few + clicks and see it as "Connected". +- G2. A project member can link one Slack channel per project and choose which + event types post there. +- G3. Configured issue events post a correctly formatted message to the linked + channel, reliably and without blocking the user's action. +- G4. Credentials and tokens are handled securely and never exposed to clients. +- G5. Disconnecting Slack cleanly stops all delivery. + +### Non-goals (v1) +- N1. Per-user DMs / mapping Devlane users to Slack users (channel-level only). +- N2. Two-way sync — creating Devlane issues from Slack, slash commands, + interactive message actions. +- N3. Threaded conversation mirroring. +- N4. Multiple channels per project or cross-project routing rules. + +--- + +## 4. Users & personas + +| Persona | Needs | Involvement | +| --- | --- | --- | +| **Instance admin** | Configure the Slack app credentials once for the whole instance | Stage 1 (setup) | +| **Workspace admin** | Connect/disconnect Slack for their workspace | Stage 2 (connect) | +| **Project member / lead** | Link a channel to a project and choose events | Stage 3 (config) | +| **Team member** | Passively receives issue updates in the shared channel | Stage 4 (consumption) | + +--- + +## 5. User stories + +- As an **instance admin**, I can enter Slack app credentials (client id, client + secret, signing secret) in instance settings so workspaces can connect. +- As a **workspace admin**, I can click "Connect" on a Slack card, approve access + in Slack, and return to see "Connected as ". +- As a **project lead**, I can pick a Slack channel for my project from a list of + channels the bot can post to, and toggle which event types are delivered. +- As a **project lead**, I can unlink a channel to stop posting for that project. +- As a **team member**, I see messages like "Priya moved ALP-42 from Todo to In + Progress" in our project channel, with a link back to the issue. +- As a **workspace admin**, I can disconnect Slack and be confident all delivery + stops immediately. + +--- + +## 6. Functional requirements + +### 6.1 Instance configuration +- FR-1. Instance admin UI provides fields for Slack `client_id`, + `client_secret`, `signing_secret`. +- FR-2. Secrets are stored encrypted in `instance_settings` (key `slack`) and + are write-only from the UI (masked, `*_set` indicators), consistent with SMTP + and GitHub App credential handling. +- FR-3. Saving new credentials takes effect without an API restart. + +### 6.2 Workspace connect (OAuth) +- FR-4. A "Connect" action starts a Slack OAuth v2 install (full-page redirect). +- FR-5. The callback verifies state (CSRF), exchanges the code for a bot token, + and stores the token + team metadata against the workspace. +- FR-6. On success the user returns to + `//settings?section=integrations` with a success indicator; on + failure, with an error message. +- FR-7. The Integrations page shows Slack as "Connected as " with a + Disconnect action, alongside the existing GitHub card. + +### 6.3 Project channel linking +- FR-8. Users can list channels the bot can post to (via Slack + `conversations.list`). +- FR-9. Users can link exactly one channel per project. +- FR-10. Users can configure which event types (assigned, state changed, + commented, mentioned, field changed) post to the channel. +- FR-11. Users can unlink the channel for a project. + +### 6.4 Notification delivery +- FR-12. When a configured issue event occurs and the project has a linked + channel with that event enabled, Devlane posts a formatted message to the + channel. +- FR-13. Delivery is asynchronous (background queue) and must never block or roll + back the originating user action. +- FR-14. Failed posts retry automatically up to 3 times, then are dropped and + logged (reusing the existing queue retry mechanism). +- FR-15. Messages include: actor, issue reference + title, the change + (e.g. before → after for state), and a link back to the issue. +- FR-16. Delivery is channel-scoped: one message per issue event per linked + channel (not one per recipient). + +### 6.5 Disconnect / teardown +- FR-17. Disconnecting Slack removes the workspace install and unlinks all + channels for that workspace; no further messages are sent. + +--- + +## 7. Experience flow (summary) + +1. **Setup (instance admin, one-time):** enter Slack app creds → stored + encrypted. +2. **Connect (workspace admin):** Connect → Slack consent → callback stores bot + token → "Connected". +3. **Configure (project lead):** pick channel + event toggles per project. +4. **Runtime (automatic):** issue event → notification fan-out → queue → Slack + `chat.postMessage` → message appears in channel. +5. **Disconnect:** removes install + channel links → delivery stops. + +_Full technical sequence for each stage is in [`SLACK_INTEGRATION_PLAN.md`](./SLACK_INTEGRATION_PLAN.md)._ + +--- + +## 8. Non-functional requirements + +- **Security:** Bot tokens and app secrets never serialized to clients + (`json:"-"`); secrets encrypted at rest; OAuth state verified to prevent CSRF. +- **Reliability:** Slack or RabbitMQ outages degrade gracefully — the user's + action always succeeds; delivery retries and fails silently (logged). +- **Consistency:** Follows the layered architecture (handler → service → store), + workspace-scoped nested URLs with trailing slashes, and instance-settings + credential resolution used elsewhere. +- **Performance:** Posting is off the request hot path (queued), so it adds no + latency to issue edits. +- **Observability:** Send attempts, successes, and failures are logged (mirroring + the mail path's `LogSendAttempt` / `LogSent` / `LogFailed`). + +--- + +## 9. Success metrics + +- **Adoption:** # of workspaces that connect Slack; # of projects with a linked + channel. +- **Delivery health:** Slack post success rate (target > 99% excluding invalid + channels); retry/drop counts stay low. +- **Engagement (proxy):** click-throughs from Slack messages back into Devlane + issues. +- **Reliability guardrail:** zero incidents of a Slack/queue failure blocking or + rolling back an issue action. + +--- + +## 10. Dependencies & assumptions + +- A registered **Slack app** (created in the Slack dashboard) providing + `client_id`, `client_secret`, `signing_secret`, and declared bot scopes + (`chat:write`, `channels:read`, `groups:read`, optionally `channels:join`). +- A **publicly reachable API URL** for Slack's OAuth redirect + (`/auth/slack/callback`) — requires a tunnel (e.g. ngrok) for local dev. +- **RabbitMQ** for the async delivery path (optional infra; feature degrades + gracefully without it — no delivery, but no errors). +- Existing notification fan-out in `service/notification.go` as the trigger + point. + +--- + +## 11. Risks & mitigations + +| Risk | Impact | Mitigation | +| --- | --- | --- | +| Slack API logical failures return HTTP 200 with `ok:false` | Silent non-delivery | Client checks `ok`; return error so retries engage | +| Bot not in target channel / private channel | Post fails | Surface clear error in channel-select UI; optionally `conversations.join` for public channels | +| OAuth redirect can't reach localhost | Blocks local testing | Document tunnel requirement; provide setup steps | +| Token leakage | Security incident | `json:"-"` on token fields; encrypted at rest; never in API responses | +| Message spam in busy projects | Channel noise | Per-event toggles; channel-scoped (not per-user); consider future rate limiting | + +--- + +## 12. Milestones (delivery order) + +1. **M1 — Foundation:** migration `000007`, model, store (no behavior). +2. **M2 — Credentials:** instance-settings `slack` key + admin UI. +3. **M3 — Connect:** OAuth provider, install/callback, routes, Connect/Disconnect UI. +4. **M4 — Channels:** list/link/unlink channel per project + event toggles UI. +5. **M5 — Delivery:** queue task, consumer, Slack client, notification hook. +6. **M6 — Hardening:** tests, error states, `npm run validate`, docs. + +Each milestone leaves the app in a working, reviewable state (one PR each). + +--- + +## 13. Acceptance criteria + +- [ ] Instance admin can enter and save Slack app credentials (secrets masked). +- [ ] Workspace admin can connect Slack via OAuth and see "Connected as ". +- [ ] A user can link and unlink a Slack channel per project. +- [ ] Configured events post a correctly formatted message to the linked channel. +- [ ] Slack access tokens are never returned to the client. +- [ ] A Slack API failure or missing RabbitMQ never blocks or rolls back the + originating Devlane action; failures are logged. +- [ ] Disconnecting Slack unlinks channels and stops delivery. +- [ ] New endpoints follow layered architecture + workspace-scoped URL/trailing- + slash conventions; migrations ship with up + down files; `npm run validate` + passes. + +--- + +## 14. Open questions + +1. **Trigger semantics** — post on every configured event when a channel is + linked (team-channel model), or only when there's at least one in-app + receiver? +2. **Default events** — which event types are enabled by default per project? +3. **Public vs private channels** — require inviting the bot, or auto-join public + channels via `channels.join`? +4. **Message richness** — plain text vs Block Kit formatting for v1. +5. **Multiple channels per project** — confirmed out of scope for v1; revisit + later? diff --git a/Task.md b/Task.md new file mode 100644 index 00000000..27ef2c7a --- /dev/null +++ b/Task.md @@ -0,0 +1,131 @@ +# Slack Integration for Channel Notifications + +| Field | Value | +| --- | --- | +| **Type** | Feature | +| **Priority** | Medium | +| **Area** | Integrations (API + Web) | +| **Status** | Proposed | + +## Summary + +Devlane has no Slack integration. Teams cannot post issue/project notifications to +Slack channels, so activity that already flows through Devlane's in-app notification +system (assignments, state changes, comments, etc.) has no path into the chat tools +where teams actually coordinate. GitHub is the only integration that is fully wired +end-to-end; Slack exists in name only. + +## Current State (Evidence) + +- **Model:** `apps/api/internal/model/integration.go` references `slack` only in a + doc comment on the `Integration` struct (`"a registered integration provider + (github, slack, ...)"`). There is no Slack-specific model, no per-project channel + table, and no token storage. +- **OAuth:** `apps/api/internal/oauth/` contains providers for `google`, `github`, + and `gitlab` only (`google.go`, `github.go`, `gitlab.go`). There is no Slack OAuth + provider and no Slack app install/callback flow. +- **Provider package:** `apps/api/internal/github/` implements a complete provider + (client, app, installations, webhook, ref parsing). There is no equivalent + `slack/` package. +- **Handlers:** `apps/api/internal/handler/integration.go` exposes generic + integration endpoints plus GitHub-specific install/sync/webhook flows. No Slack + handler or routes exist. +- **Services:** `apps/api/internal/service/` has `integration.go`, `github_sync.go`, + and `github_events.go`, plus `notification.go` (which fans out in-app/email + notifications via `Emit*` methods and the RabbitMQ `queue.Publisher`). Nothing + sends to Slack. +- **Instance settings:** `apps/api/internal/model/instance_setting.go` is the + key-value (JSONB) store used for SMTP and OAuth provider credentials, resolved at + request time. There is no Slack app credential key. +- **Web UI:** `apps/web/src/components/integrations/` contains only GitHub + components (`IntegrationsSection.tsx`, `RepoSyncSettingsModal.tsx`). + `IntegrationsSection` is hardcoded to a single `github` provider (`installed.find( + (wi) => wi.provider === 'github')`) with no Slack card, connect button, or + channel-select UI. + +**GitHub integration is complete** and is the reference pattern to follow for +layering, routing conventions, and instance-settings-based credential resolution. + +## Problem + +There is no supported way to deliver Devlane notifications to Slack. Users who want +issue activity surfaced in a Slack channel must build their own polling or webhook +tooling. This is a gap relative to comparable tools and relative to Devlane's own +GitHub integration, which is fully productionized. + +## Proposed Scope + +Deliver Slack notifications following the existing GitHub integration patterns +(handler → service → store layering, workspace-scoped nested URLs with trailing +slashes, instance-settings credential resolution, graceful degradation when optional +infra is absent). + +1. **Data model + migration** (`apps/api/internal/model/`, `apps/api/migrations/`): + - A per-project Slack channel/token model (workspace + project scoped) holding the + Slack team/workspace id, channel id + name, and the bot/OAuth access token + (stored in a way consistent with how existing secrets/credentials are handled — + tokens must never be serialized back to the client, matching the + `WebhookSecret` / `Credentials` `json:"-"` pattern). + - Reuse `integrations` / `workspace_integrations` where it fits (register `slack` + as a provider row; store the install under `workspace_integrations`), mirroring + GitHub. + - Add both `NNNNNN_.up.sql` and `.down.sql`; never edit merged migrations. + +2. **Slack OAuth + install handler** (`apps/api/internal/oauth/slack.go`, + `apps/api/internal/slack/`, `apps/api/internal/handler/`): + - Slack OAuth provider and an install/callback flow that mirrors the GitHub + install handler, redirecting back to + `//settings?section=integrations` with `?connected=slack` / `?error=...`. + - App credentials (client id/secret, signing secret) resolved from + `instance_settings` at request time, not env. + +3. **Notification sender** (`apps/api/internal/slack/` + `apps/api/internal/service/`): + - A Slack client that posts messages to a channel (`chat.postMessage`). + - Hook into the notification fan-out so relevant events (config-driven per + project) are delivered to the linked channel. Prefer routing through the + existing background `queue.Publisher` path (as email notifications do) so a + Slack outage can't block or roll back the user's action; degrade gracefully if + RabbitMQ is absent. + +4. **Web UI** (`apps/web/src/components/integrations/`, + `apps/web/src/services/integrationService.ts`, `apps/web/src/api/types.ts`): + - Add a Slack provider card to `IntegrationsSection` (Connect / Disconnect), + generalizing the currently GitHub-only lookup. + - A per-project channel-select UI (list channels the app can post to; link/unlink + a channel per project), following the linked-repositories panel pattern. + - New service methods on `integrationService` (install URL, list channels, link / + unlink channel), calling through `apiClient`. + +5. **Instance admin settings** + (`apps/web/src/pages/instance-admin/`, `instance_settings`): + - Admin UI + backend key to store Slack app credentials, matching how SMTP/OAuth + provider creds are configured today. + +## Out of Scope (initial) + +- Two-way sync (creating Devlane issues from Slack, slash commands, interactive + message actions). +- Slack DMs / per-user notification routing (channel-level only for v1). +- Threaded conversation mirroring. + +## Acceptance Criteria + +- An instance admin can enter Slack app credentials in instance settings. +- A workspace admin can connect Slack via OAuth and see it as "Connected" alongside + GitHub in workspace settings → Integrations. +- A user can select/link a Slack channel per project and unlink it. +- Configured notification events post a correctly formatted message to the linked + channel. +- Slack access tokens are never returned to the client. +- A Slack API failure (or missing RabbitMQ) never blocks or rolls back the + originating Devlane action; failures are logged. +- Disconnecting Slack unlinks channels and stops delivery. +- New endpoints follow the layered architecture and workspace-scoped URL/trailing- + slash conventions; migrations ship with up + down files; `npm run validate` passes. + +## Touched Areas (per repo conventions) + +`model/` → migration → `store/` → `service/` → `handler/` → register in +`router/router.go` → `oauth/slack.go` + new `slack/` package → web +`services/integrationService.ts` → `components/integrations/` → instance-admin UI + +`instance_settings` key. diff --git a/Todo.md b/Todo.md new file mode 100644 index 00000000..96f27ef5 --- /dev/null +++ b/Todo.md @@ -0,0 +1,6 @@ +1. Instance setting configuration - Done +2. Database Migration: Need a new table `slack_channel_links` - Done +3. Go Models & Store: Appended Slack models to `model/integration.go` and `store/integration.go` - Done +4. Slack OAuth & Install Handler: Create `oauth/slack.go` and update `handler/integration.go` so users can authenticate with Slack - Done +5. Slack Notification Logic: Build a Slack client and wire it up with the background queue (RabbitMQ) to send channel messages - Done +6. Web UI for Channels: Add Slack to `IntegrationsSection.tsx` and create `SlackChannelSettingsModal.tsx` for linking projects to channels - Done \ No newline at end of file diff --git a/apps/api/internal/handler/instance.go b/apps/api/internal/handler/instance.go index e4c1bd86..d8c4d5f3 100644 --- a/apps/api/internal/handler/instance.go +++ b/apps/api/internal/handler/instance.go @@ -23,7 +23,7 @@ import ( // Allowed instance setting section keys (must match migration seed). var allowedSettingKeys = map[string]bool{ "general": true, "email": true, "auth": true, "oauth": true, "ai": true, "image": true, - "github_app": true, + "github_app": true, "slack_app": true, } // InstanceHandler serves instance setup (first-run); no auth required. @@ -269,7 +269,7 @@ func (h *InstanceSettingsHandler) GetSettings(c *gin.Context) { out[k] = decryptSectionSecretsInternal(k, row.Value) } // Ensure all sections exist with defaults (migration seed may not have run if DB was created before seed) - for _, key := range []string{"general", "email", "auth", "oauth", "ai", "image", "github_app"} { + for _, key := range []string{"general", "email", "auth", "oauth", "ai", "image", "github_app", "slack_app"} { if _, ok := out[key]; !ok { out[key] = defaultSettingValue(key) } @@ -284,6 +284,7 @@ var secretKeysBySection = map[string][]string{ "ai": {"api_key"}, "image": {"unsplash_access_key"}, "github_app": {"private_key", "client_secret", "webhook_secret"}, + "slack_app": {"client_secret", "signing_secret"}, } // decryptSectionSecretsInternal returns a copy of m with secret fields decrypted. @@ -328,6 +329,8 @@ func defaultSettingValue(key string) model.JSONMap { "app_id": "", "app_name": "", "client_id": "", "client_secret_set": false, "private_key_set": false, "webhook_secret_set": false, } + case "slack_app": + return model.JSONMap{"client_id": "", "client_secret_set": false, "signing_secret_set": false} default: return model.JSONMap{} } @@ -517,6 +520,41 @@ func (h *InstanceSettingsHandler) UpdateSetting(c *gin.Context) { setSecret("webhook_secret", "webhook_secret_set") value = merged } + if key == "slack_app" { + existing, _ := h.Settings.Get(c.Request.Context(), "slack_app") + merged := model.JSONMap{} + + if existing != nil { + for k, v := range existing.Value { + merged[k] = v + } + } else { + for k, v := range defaultSettingValue("slack_app") { + merged[k] = v + } + } + + /* plain feilds */ + for _, feild := range []string{"client_id"} { + if v, ok := req.Value[feild]; ok { + merged[feild] = v + } + } + + /* Secret fields (Encrypt & Set flag) */ + setSecret := func(feild, setKey string) { + if v, ok := req.Value[feild]; ok { + if s, ok := v.(string); ok && s != "" { + merged[feild] = crypto.EncryptOrPlain(s) + merged[setKey] = true + } + } + } + + setSecret("client_secret", "client_secret_set") + setSecret("signing_secret", "signing_secret_set") + value = merged + } if err := h.Settings.Upsert(c.Request.Context(), key, value); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"}) return diff --git a/apps/web/package.json b/apps/web/package.json index acfe667d..0d519f1c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "start": "vite", "build": "tsc -b && vite build", "typecheck": "tsc -b --noEmit", "lint": "eslint --max-warnings=0 .", diff --git a/apps/web/src/api/types.ts b/apps/web/src/api/types.ts index ddaae18c..a6400070 100644 --- a/apps/web/src/api/types.ts +++ b/apps/web/src/api/types.ts @@ -607,6 +607,15 @@ export interface InstanceGitHubAppSection { webhook_secret_set?: boolean; } +/* Slack App config (instance admin). Secrets are never echoed back. */ +export interface InstanceSlackAppSection { + client_id?: string; + client_secret?: string; + client_secret_set?: boolean; + signing_secret?: string; + signing_secret_set?: boolean; +} + /** Available integration provider, returned by GET /api/integrations/. */ export interface IntegrationApiResponse { id: string; diff --git a/apps/web/src/components/layout/InstanceAdminLayout.tsx b/apps/web/src/components/layout/InstanceAdminLayout.tsx index 3a463a85..455d3fe8 100644 --- a/apps/web/src/components/layout/InstanceAdminLayout.tsx +++ b/apps/web/src/components/layout/InstanceAdminLayout.tsx @@ -281,6 +281,7 @@ const AUTH_SUB_LABEL: Record = { const INTEGRATIONS_SUB_LABEL: Record = { github: 'GitHub', + slack: 'Slack', }; export function InstanceAdminLayout() { diff --git a/apps/web/src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx b/apps/web/src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx new file mode 100644 index 00000000..02ed3b1e --- /dev/null +++ b/apps/web/src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx @@ -0,0 +1,305 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Eye, EyeOff } from 'lucide-react'; +import { Button, Input } from '../../components/ui'; +import { InstanceAdminCopyRow } from '../../components/instance-admin'; +import { instanceSettingsService } from '../../services/instanceService'; +import { authService } from '../../services/authService'; +import { getApiErrorMessage } from '../../api/client'; +import { useDocumentTitle } from '../../hooks/useDocumentTitle'; +import type { InstanceSlackAppSection } from '../../api/types'; + +const IconSlack = () => ( + + + + + + +); + +/** + * Configure the Slack App credentials for the whole instance. Until this is + * filled in, no workspace can connect Slack. Secrets (client secret, signing + * secret) are encrypted at rest and never echoed back from the API — the form + * clears the field after save and shows a *_set badge instead. + */ +export function InstanceAdminIntegrationSlackPage() { + const navigate = useNavigate(); + + // Form state. Secrets default to empty; if the corresponding *_set is true, + // the placeholder tells the user "(unchanged if blank)". + const [clientID, setClientID] = useState(''); + const [clientSecret, setClientSecret] = useState(''); + const [clientSecretSet, setClientSecretSet] = useState(false); + const [signingSecret, setSigningSecret] = useState(''); + const [signingSecretSet, setSigningSecretSet] = useState(false); + + // For the snapshot we compare against to compute isDirty. + const [initial, setInitial] = useState({ + clientID: '', + }); + + const [showClientSecret, setShowClientSecret] = useState(false); + const [showSigningSecret, setShowSigningSecret] = useState(false); + + // URL the admin pastes into the Slack App's "OAuth & Permissions" settings. + const [oauthRedirectBase, setOauthRedirectBase] = useState(''); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + useDocumentTitle('Slack integration'); + + const redirectUrl = useMemo( + () => (oauthRedirectBase ? `${oauthRedirectBase}/auth/slack/callback` : ''), + [oauthRedirectBase], + ); + + useEffect(() => { + let cancelled = false; + Promise.all([instanceSettingsService.getSettings(), authService.getAuthConfig()]) + .then(([settings, cfg]) => { + if (cancelled) return; + const s = (settings.slack_app || {}) as InstanceSlackAppSection; + setClientID(s.client_id ?? ''); + setClientSecretSet(s.client_secret_set ?? false); + setSigningSecretSet(s.signing_secret_set ?? false); + setInitial({ + clientID: s.client_id ?? '', + }); + if (cfg.oauth_redirect_base) setOauthRedirectBase(cfg.oauth_redirect_base); + else if (typeof window !== 'undefined') setOauthRedirectBase(window.location.origin); + }) + .catch((err) => { + if (!cancelled) setError(getApiErrorMessage(err)); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + const isDirty = + clientID !== initial.clientID || clientSecret.length > 0 || signingSecret.length > 0; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setSuccess(''); + setSaving(true); + + const payload: InstanceSlackAppSection = { + client_id: clientID.trim(), + }; + if (clientSecret.trim()) payload.client_secret = clientSecret.trim(); + if (signingSecret.trim()) payload.signing_secret = signingSecret.trim(); + + instanceSettingsService + .updateSection('slack_app', payload as import('../../api/types').InstanceSettingSectionValue) + .then((res) => { + const v = (res.value || {}) as InstanceSlackAppSection; + setClientID(v.client_id ?? ''); + setClientSecretSet(v.client_secret_set ?? false); + setSigningSecretSet(v.signing_secret_set ?? false); + setInitial({ + clientID: v.client_id ?? '', + }); + // Clear local secret fields — they've been saved. + setClientSecret(''); + setSigningSecret(''); + setSuccess('Slack App settings saved. Workspaces can now connect.'); + }) + .catch((err) => setError(getApiErrorMessage(err))) + .finally(() => setSaving(false)); + }; + + if (loading) { + return ( +
+
+
+
+
+
+
+ ); + } + + return ( +
+
+ + + +
+

Slack App

+

+ Register a Slack App and paste its credentials here. The App is the bridge that lets + Devlane exchange notifications, synchronize activity, and enable Slack-powered workflows + across all workspaces on this instance. +

+
+
+ + {error &&

{error}

} + {success &&

{success}

} + +
+

First time? Quick setup:

+
    +
  1. + Open{' '} + + Slack API → Your Apps → Create New App + + . +
  2. + +
  3. + {' '} + Under OAuth & Permissions, add the Redirect URL + provided below. +
  4. +
  5. + Add the required bot scopes: chat:write,{' '} + channels:read,{' '} + groups:read. +
  6. +
  7. Install the app in your Slack workspace.
  8. +
  9. + Copy the Client ID, Client Secret, and Signing Secret from{' '} + Basic Information into Devlane. +
  10. +
  11. + Still need help? See the{' '} + + Slack Quickstart Guide + + . +
  12. +
+
+ +
+
+

+ Credentials from your Slack App +

+
+ setClientID(e.target.value)} + autoComplete="off" + placeholder="e.g. 1234567890.1234567890123" + /> +

+ Found under Basic Information → App Credentials. + This value is public and safe to share. +

+ +
+ setClientSecret(e.target.value)} + autoComplete="new-password" + placeholder={clientSecretSet ? '(unchanged if left blank)' : 'Enter client secret'} + /> + +
+

+ Sent with the Client ID during the OAuth token exchange ( + oauth.v2.access). Stored encrypted at rest. +

+ +
+ setSigningSecret(e.target.value)} + autoComplete="new-password" + placeholder={ + signingSecretSet ? '(unchanged if left blank)' : 'Enter signing secret' + } + /> + +
+

+ Used to verify that inbound requests genuinely come from Slack. Stored encrypted at + rest (set INSTANCE_ENCRYPTION_KEY on the API). +

+
+
+ +
+

+ Devlane URLs to paste into the Slack App +

+
+ +
+
+ +
+ + +
+
+
+ ); +} diff --git a/apps/web/src/pages/instance-admin/InstanceAdminIntegrationsPage.tsx b/apps/web/src/pages/instance-admin/InstanceAdminIntegrationsPage.tsx index fd11e166..4573a11e 100644 --- a/apps/web/src/pages/instance-admin/InstanceAdminIntegrationsPage.tsx +++ b/apps/web/src/pages/instance-admin/InstanceAdminIntegrationsPage.tsx @@ -6,21 +6,44 @@ import { Skeleton } from '../../components/ui'; import { instanceSettingsService } from '../../services/instanceService'; import { getApiErrorMessage } from '../../api/client'; import { useDocumentTitle } from '../../hooks/useDocumentTitle'; -import type { InstanceGitHubAppSection } from '../../api/types'; +import type { InstanceGitHubAppSection, InstanceSlackAppSection } from '../../api/types'; const IconGitHub = () => ( ); +const IconSlack = () => ( + + + + + + +); + +type ProviderCategory = 'source-control' | 'messaging'; interface ProviderRow { - id: 'github'; + id: 'github' | 'slack'; name: string; desc: string; Icon: () => React.ReactElement; editPath: string; configured: boolean; + category: ProviderCategory; } function isGitHubAppConfigured(s: InstanceGitHubAppSection): boolean { @@ -34,9 +57,14 @@ function isGitHubAppConfigured(s: InstanceGitHubAppSection): boolean { ); } +function isSlackConfigured(s: InstanceSlackAppSection): boolean { + return !!(s.client_id && s.client_secret_set && s.signing_secret_set); +} + export function InstanceAdminIntegrationsPage() { const { t } = useTranslation(); const [github, setGithub] = useState({}); + const [slack, setSlack] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); useDocumentTitle(t('instanceAdmin.integrations.documentTitle', 'Integrations')); @@ -49,6 +77,8 @@ export function InstanceAdminIntegrationsPage() { if (cancelled) return; const g = (settings.github_app || {}) as InstanceGitHubAppSection; setGithub(g); + const sl = (settings.slack || {}) as InstanceSlackAppSection; + setSlack(sl); }) .catch((err) => { if (!cancelled) setError(getApiErrorMessage(err)); @@ -72,6 +102,16 @@ export function InstanceAdminIntegrationsPage() { Icon: IconGitHub, editPath: '/instance-admin/integrations/github', configured: isGitHubAppConfigured(github), + category: 'source-control', + }, + { + id: 'slack', + name: 'Slack', + desc: 'Post Devlane notifications (assigned, state changed, commented, mentioned) to a Slack channel per project.', + Icon: IconSlack, + editPath: '/instance-admin/integrations/slack', + configured: isSlackConfigured(slack), + category: 'messaging', }, ]; @@ -83,7 +123,7 @@ export function InstanceAdminIntegrationsPage() {
    - {[1].map((i) => ( + {[1, 2].map((i) => (
  • ), ); +const InstanceAdminIntegrationSlackPage = lazy(() => + import('../pages/instance-admin').then((m) => + page({ InstanceAdminIntegrationSlackPage: m.InstanceAdminIntegrationSlackPage }), + ), +); + const InstanceSetupWelcomePage = lazy(() => import('../pages/setup').then((m) => page({ InstanceSetupWelcomePage: m.InstanceSetupWelcomePage }), @@ -399,6 +405,14 @@ const router = createBrowserRouter([ ), }, + { + path: 'integrations/slack', + element: ( + }> + + + ), + }, ], }, { diff --git a/instance_settings_202607251917.json b/instance_settings_202607251917.json new file mode 100644 index 00000000..a425406c --- /dev/null +++ b/instance_settings_202607251917.json @@ -0,0 +1,45 @@ +{ +"instance_settings": [ + { + "key" : "auth", + "value" : "{\"github\": false, \"gitlab\": false, \"google\": false, \"password\": true, \"magic_code\": true, \"allow_public_signup\": true}", + "created_at" : "2026-07-10T22:00:45.960Z", + "updated_at" : "2026-07-10T22:00:45.960Z" + }, + { + "key" : "ai", + "value" : "{\"model\": \"gpt-4o-mini\", \"api_key_set\": false}", + "created_at" : "2026-07-10T22:00:45.960Z", + "updated_at" : "2026-07-10T22:00:45.960Z" + }, + { + "key" : "image", + "value" : "{\"unsplash_access_key_set\": false}", + "created_at" : "2026-07-10T22:00:45.960Z", + "updated_at" : "2026-07-10T22:00:45.960Z" + }, + { + "key" : "general", + "value" : "{\"admin_email\": \"indravihar15722@gmail.com\", \"instance_id\": \"072d575f304d529a4d624389\", \"instance_name\": \"Fractal Analytics\", \"only_admin_can_create_workspace\": false}", + "created_at" : "0001-12-30T00:00:00.000Z", + "updated_at" : "2026-07-10T22:05:54.628Z" + }, + { + "key" : "email", + "value" : "{\"host\": \"localhost\", \"port\": \"1025\", \"security\": \"None\", \"username\": \"\", \"password_set\": false, \"sender_email\": \"dev@devlane.local\"}", + "created_at" : "0001-12-30T00:00:00.000Z", + "updated_at" : "2026-07-11T14:24:52.647Z" + }, + { + "key" : "github_app", + "value" : "{\"app_id\": \"4342116\", \"app_name\": \"devlane-local-app\", \"client_id\": \"Iv23linSrDFBt33MTPkV\", \"private_key\": \"enc:8TzbGXT+r2XM\/KBT8ExZYErVyQehRdl80+TRwYmq6hvKlEgNx9oUDSybAIOrM2JIpYHPPk+dnmmQi1M3LEii0IRNqxAqwxumFODwXa9DTKfTs+0y3omojScenm1jHmW3WrgZn3aa8DJODB+v5ZlFmcSaBMRt8YOFDvf26gXVenkcMXh\/P7Wdrf3\/etOmw00cXq2fxBmYoyxDeAEkWLRxcCHBliHH4PjNcUaNaf3umfbxKEl6WUinSlbpjdJTp\/z5C\/0NzVEPcTVwvfhL1pAPxwi0ZTeZJ+mDw41hrQcx6gr1Da4nbJrFQu1w9I4it8oHA+sQBthV0rAZy3lZuYSEjAnKQ1nB3JugCJc0cyIVz0lC+O3IJgICsmIpqNmxMHSRDbCrYV6OgC1cQHXNYtURf2Iye5T0upvThuH\/WM5tzfu3GAZS\/8L4GHSxv87fmsiO30qGGpY6k6vFoPiOMPDojyUW5jt64T5yznkoMwYzSatFIQlNAULLiBVaXVQ7JXJ45gwY\/WI+dbcH2RMn2HO2syHKHeRvmFDzbed++EHyxQya3Kh6KZFF\/ibw0NS7GtDK1w8i0owIM57b42x4uUBSVAchGpR9qCa4uRy7OqTajXthIQZ3UVljOfDmiwWNZ8Z+fMKsZLueNL0LrnwKHConRhRQ3V8ojSyO8JWfFO0bxZmN+i5lnDUKWiuYcpyNQeX3v7qGll8MoOwbxOjq0SHYdewgnrRfjLMvfQ7uGODm9PqHV8MGrinvGquxfg0JS0DvTsOER14XmyZzIvSsra+h2RVzZ5DSppZ8VTpZAAnAPYv3+gnROKV2i9orRsSVsaAY10KNZoq9vDHghWoh9IutjI6bmLOniQur+Clc9rcC9lPt4A1\/5pMVpQ7x97VMUF53eGkXrsgdZXG9Hok5K1uc+JeZwY5PPzwkmjyWQWdAtppCbtJJPLjA1lvFYIn5r7niHl8ondcGpxNk7HViJKqOxTfAOSz3gLX1OXUlretLFyzZ6Ll+dyPLP14nP3zgNfLgmIfDtpahiRKT1WLQwY2CjilBJJxIS0k+mb9WVU17JP6cB682\/w3x00oT7V0AUOBDXIvekFGn2MfAfokcxzwgzQw31tyeYUkxqW+Pa3I3zTuPD48Z0+sXDtmYW3AT4wqqAxg4DRvluG9kwWK3jHcTbUKd0tmlf9+Zi7YtIbrER89Gh0culi\/jSIWhCkjTWfApb24TASzMrZewf9D1UC9hYtMpuJ8GI4l0u5QgG0B0GAkdTD33F+Qrq0DTeGFcxY1kuuNXAcxYT1IsKTaPYrFVTQuNBZaLtt4Iwkiy0MCf4hAmr84BXNrg+RYc+e5YapLJWtFl\/i4udouTJnylnezXZmnjxCHwXaYfGRMqVIsUqE0qmKn4JwNn1bwMTgBLY9bzc9kQN0ZAcP0TBPzLu4lix7GrNlJDhGEYhLqNPRXSQrG6FahaqSgsPAUHbK7mZAkhHFTxpoNUxRmFzSIXK706Z73BAOsMGFkVYmq7y0hhNo2oipx8ZNH8OuMzX5lwnHimhHvYK+yKdLEPp7V2wlDRm8EUqy5VjPvgnh83Y37ukkyH1ZNLyL+MNMBsd925MG2Hf3k+eT4b06gAmiUGiLfZgJn3n5Eq4btvTtySek8bdj6b3WiPA6V+LKmE7\/FbsrLfB3eIOQ24HcdIGnzPDxXVt5J2A0uLFQKBEWufoTFRy3xi0cOZEXrxKamodUf9LRORQuTl50r1xbc4RDVGt6b43ioNy\/zE1H0tW1ET4kytzpPcFhrpHmfKA9IubEwvgHIwoVl86uLR2nVBFOsNAhr6L9HkmT2nTFtimTBeLuzxfprDHJKVMZ+S7K486kPFjMak7HeKu9R\/+8s5q5b01WTgfPUnHYPBoJXD6+ssJwiaZcOS7QnE4MgbetaKpLh8ayZlZRVlJRL19ePscu7xWlkoVa62CN6wGOnTmC5FCsi0jcOyvDfrPhaBbO5GOp+fT4RAFzPeY9TxUksL+jA3UYtt+mP9kLunWIF1lcj7jIl9MJmUhItQZaJ8pmbL2LyScG8HWKW3Va0FV8vInz0vB0lD+cs401NGqdJecquFto7mr6Lh2jZHMmW+SqKOWzuuM25vm+EM2RRYaO44YXKG\/Qumo5BRR\/YcluGDx6PwHrN4eGC9eZvthkAS6Sut7me5TTUIK5AX8u6rmegnhoEWilE+dnXY5funtdKQG\/CtAxHwLF5J9JOCZZzwXVh1aQ3R47CU7Pvj8+OEu6aUsXxK7doou5eg0TKy5Lw=\", \"client_secret\": \"enc:h5BG6yrS747Bz5eT7PAmqC60GVQJSGMR+d7v7AvU2R0kWzLq2rGXoYsHslOmyPQuZTTE\/81FqmKMaEvtsFLOIjnxeXA=\", \"webhook_secret\": \"enc:qI2W5n5yX4HRsaERa0yeff3pEXphb2v0WM9Zuwt6l8uWll83qMM=\", \"private_key_set\": true, \"client_secret_set\": true, \"webhook_secret_set\": true}", + "created_at" : "0001-12-30T00:00:00.000Z", + "updated_at" : "2026-07-20T00:25:55.845Z" + }, + { + "key" : "slack_app", + "value" : "{\"client_id\": \"adfadsf\", \"client_secret\": \"enc:0OfeT2Psqj0\/XDah3v+0NK3Wo4qenbP3hB06ZxWy2h7pjQ==\", \"signing_secret\": \"enc:6lVQruceFNWNuYkNnhb7togJHZajr\/NLutSm50byAQaXA94Q\", \"client_secret_set\": true, \"signing_secret_set\": true}", + "created_at" : "0001-12-30T00:00:00.000Z", + "updated_at" : "2026-07-25T08:41:23.530Z" + } +]} diff --git a/smee.log b/smee.log new file mode 100644 index 00000000..34c56c18 --- /dev/null +++ b/smee.log @@ -0,0 +1,12 @@ +Connected to https://smee.io/xyz123 +Forwarding https://smee.io/xyz123 to http://localhost:8080/webhooks/github +POST http://localhost:8080/webhooks/github - 200 +POST http://localhost:8080/webhooks/github - 200 +POST http://localhost:8080/webhooks/github - 200 +POST http://localhost:8080/webhooks/github - 200 +POST http://localhost:8080/webhooks/github - 200 +POST http://localhost:8080/webhooks/github - 200 +POST http://localhost:8080/webhooks/github - 200 +POST http://localhost:8080/webhooks/github - 200 +POST http://localhost:8080/webhooks/github - 200 +POST http://localhost:8080/webhooks/github - 200 From 284025ae3250772de36d87cbe3b064d90ff0df83 Mon Sep 17 00:00:00 2001 From: Amit kumar Date: Tue, 28 Jul 2026 03:13:08 +0530 Subject: [PATCH 2/4] feat: add i18n support to the Slack integration admin page --- .../InstanceAdminIntegrationSlackPage.tsx | 109 +++++++++++------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/apps/web/src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx b/apps/web/src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx index 02ed3b1e..2695aba0 100644 --- a/apps/web/src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx +++ b/apps/web/src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx @@ -8,6 +8,7 @@ import { authService } from '../../services/authService'; import { getApiErrorMessage } from '../../api/client'; import { useDocumentTitle } from '../../hooks/useDocumentTitle'; import type { InstanceSlackAppSection } from '../../api/types'; +import { useTranslation, Trans } from 'react-i18next'; const IconSlack = () => ( @@ -37,6 +38,7 @@ const IconSlack = () => ( * clears the field after save and shows a *_set badge instead. */ export function InstanceAdminIntegrationSlackPage() { + const { t } = useTranslation(); const navigate = useNavigate(); // Form state. Secrets default to empty; if the corresponding *_set is true, @@ -148,11 +150,14 @@ export function InstanceAdminIntegrationSlackPage() {
    -

    Slack App

    +

    + {t('instanceAdmin.slack.title', 'Slack App')} +

    - Register a Slack App and paste its credentials here. The App is the bridge that lets - Devlane exchange notifications, synchronize activity, and enable Slack-powered workflows - across all workspaces on this instance. + {t( + 'instanceAdmin.slack.description', + 'Register a Slack App and paste its credentials here. The App is the bridge that lets Devlane exchange notifications, synchronize activity, and enable Slack-powered workflows across all workspaces on this instance.', + )}

@@ -161,47 +166,59 @@ export function InstanceAdminIntegrationSlackPage() { {success &&

{success}

}
-

First time? Quick setup:

+

+ {t('instanceAdmin.slack.quickSetup', 'First time? Quick setup:')} +

  1. - Open{' '} - - Slack API → Your Apps → Create New App - - . + + Open{' '} + + Slack API → Your Apps → Create New App + + . +
  2. - {' '} - Under OAuth & Permissions, add the Redirect URL - provided below. + + {' '} + Under OAuth & Permissions, add the Redirect URL + provided below. +
  3. - Add the required bot scopes: chat:write,{' '} - channels:read,{' '} - groups:read. + + Add the required bot scopes: chat:write,{' '} + channels:read,{' '} + groups:read. +
  4. -
  5. Install the app in your Slack workspace.
  6. +
  7. {t('instanceAdmin.slack.step4', 'Install the app in your Slack workspace.')}
  8. - Copy the Client ID, Client Secret, and Signing Secret from{' '} - Basic Information into Devlane. + + Copy the Client ID, Client Secret, and Signing Secret from{' '} + Basic Information into Devlane. +
  9. - Still need help? See the{' '} - - Slack Quickstart Guide - - . + + Still need help? See the{' '} + + Slack Quickstart Guide + + . +
@@ -209,7 +226,7 @@ export function InstanceAdminIntegrationSlackPage() {

- Credentials from your Slack App + {t('instanceAdmin.slack.credentialsTitle', 'Credentials from your Slack App')}

- Found under Basic Information → App Credentials. - This value is public and safe to share. + + Found under Basic Information → App Credentials. + This value is public and safe to share. +

@@ -243,8 +262,10 @@ export function InstanceAdminIntegrationSlackPage() {

- Sent with the Client ID during the OAuth token exchange ( - oauth.v2.access). Stored encrypted at rest. + + Sent with the Client ID during the OAuth token exchange ( + oauth.v2.access). Stored encrypted at rest. +

@@ -268,15 +289,17 @@ export function InstanceAdminIntegrationSlackPage() {

- Used to verify that inbound requests genuinely come from Slack. Stored encrypted at - rest (set INSTANCE_ENCRYPTION_KEY on the API). + + Used to verify that inbound requests genuinely come from Slack. Stored encrypted at + rest (set INSTANCE_ENCRYPTION_KEY on the API). +

- Devlane URLs to paste into the Slack App + {t('instanceAdmin.slack.urlsTitle', 'Devlane URLs to paste into the Slack App')}

void navigate('/instance-admin/integrations')} className="bg-transparent text-(--txt-secondary) shadow-none hover:bg-(--bg-layer-1-hover) hover:text-(--txt-primary)" > - Go back + {t('instanceAdmin.slack.goBack', 'Go back')}
From dda9505650f1d348328bef41d34daa2e1a58c771 Mon Sep 17 00:00:00 2001 From: Amit kumar Date: Tue, 28 Jul 2026 23:40:18 +0530 Subject: [PATCH 3/4] chore: delete unnecessary files --- SLACK_INTEGRATION_PLAN.md | 362 ---------------------------- SLACK_INTEGRATION_PRD.md | 246 ------------------- instance_settings_202607251917.json | 45 ---- smee.log | 12 - 4 files changed, 665 deletions(-) delete mode 100644 SLACK_INTEGRATION_PLAN.md delete mode 100644 SLACK_INTEGRATION_PRD.md delete mode 100644 instance_settings_202607251917.json delete mode 100644 smee.log diff --git a/SLACK_INTEGRATION_PLAN.md b/SLACK_INTEGRATION_PLAN.md deleted file mode 100644 index eba817fc..00000000 --- a/SLACK_INTEGRATION_PLAN.md +++ /dev/null @@ -1,362 +0,0 @@ -# Slack Integration — Implementation Plan - -Delivering Devlane notifications to Slack channels. This plan follows the task -spec in [`Taks.md`](./Taks.md) and mirrors two existing, fully-wired patterns in -the codebase: - -- **GitHub integration** → the *integration scaffolding* (OAuth, install flow, - provider package, `integrations` / `workspace_integrations` storage, layered - `handler → service → store`, workspace-scoped nested routes with trailing - slashes, instance-settings credential resolution). -- **Email notifications** → the *delivery mechanism* (fan-out in - `service/notification.go`, background `queue.Publisher` → RabbitMQ → - `queue.Consumer` with the built-in 3-retry machinery, graceful degradation - when RabbitMQ is absent). - -The result fuses the two: Slack install/config looks like GitHub; Slack message -posting rides the same queue path as email. - ---- - -## 1. Scope (v1) - -**In scope** -- Register `slack` as an integration provider and let a workspace connect it via - Slack OAuth. -- Store the bot token + team info per workspace install; link a **Slack channel - per project**. -- On notification events (assigned / state changed / commented / mentioned / - field changed), post a formatted message to the linked channel via - `chat.postMessage`, routed through the background queue. -- Instance-admin UI to store Slack app credentials in `instance_settings`. -- Web UI: Slack provider card (Connect/Disconnect) + per-project channel - select/unlink. - -**Out of scope (v1)** — matches `Taks.md` -- Per-user DMs / mapping Devlane users ↔ Slack users (channel-level only). -- Two-way sync, slash commands, interactive actions. -- Threaded conversation mirroring. - -> Note on behavior: v1 posts to a **shared project channel**, not a DM to the -> assignee. The email/in-app notifications remain the per-user path. - ---- - -## 2. Architecture at a glance - -``` -Issue mutation (assign / state / comment / field change) - → IssueService / CommentService - → NotificationService.Issue* (service/notification.go) - → emit() ── in-app rows (unchanged) - ├─ enqueueNotificationEmails → queue.PublishSendEmail (existing) - └─ enqueueSlackNotifications → queue.PublishSlackPost (NEW) - │ - RabbitMQ "devlane.slack" queue (NEW) - │ - queue.Consumer → HandleSlackPost (NEW) - │ - slack.PostMessage → chat.postMessage (NEW) -``` - -Credential/config resolution mirrors GitHub: Slack **app** creds (client id / -secret / signing secret) live in `instance_settings` under a new `slack` key; -the per-workspace **bot token** lives on the `workspace_integrations` row; the -per-project **channel** lives in a new table. - ---- - -## 3. Backend - -### 3.1 Data model + migration - -**New migration:** `apps/api/migrations/000007_slack_integration.{up,down}.sql` -(next free number after `000006_instance_admins`). Never edit merged migrations; -ship both up and down. - -- **Seed the provider row** into `integrations` (mirrors how `github` is - registered): - ```sql - INSERT INTO integrations (title, provider, network, verified) - VALUES ('Slack', 'slack', 1, true) - ON CONFLICT (provider) DO NOTHING; - ``` -- **Reuse `workspace_integrations`** for the install. Store on the existing row: - - `account_login` → Slack team/workspace name - - `metadata` (jsonb) → `{ "team_id": "T…", "bot_user_id": "U…", "scope": "…" }` - - `config` (jsonb) → bot access token, encrypted. Follow the - `WebhookSecret` / `Credentials` `json:"-"` rule so the token is **never** - serialized to the client. -- **New table `slack_channel_links`** (per project ↔ channel), mirroring - `github_repository_syncs`: - ```sql - CREATE TABLE slack_channel_links ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_integration_id UUID NOT NULL REFERENCES workspace_integrations (id) ON DELETE CASCADE, - project_id UUID NOT NULL REFERENCES projects (id) ON DELETE CASCADE, - workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE, - channel_id VARCHAR(64) NOT NULL, -- Slack "C…" id - channel_name VARCHAR(255) NOT NULL, - events JSONB NOT NULL DEFAULT '{}', -- per-event enable flags - actor_id UUID NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ, - created_by_id UUID, - updated_by_id UUID, - UNIQUE (project_id, deleted_at) -- one active channel link per project - ); - ``` - -**New model file:** `apps/api/internal/model/slack.go` -- `type SlackChannelLink struct { … }` with `TableName() "slack_channel_links"` - and a `BeforeCreate` UUID hook — copy the shape from - `model/integration.go`'s `GithubRepositorySync`. Keep the token field - (if any is duplicated here) `json:"-"`. - -### 3.2 Instance settings (Slack app credentials) - -**`apps/api/internal/handler/instance.go`** -- Add `slack` to `allowedSettingKeys` (line ~25) and to the section list in - `GetSettings` (line ~272). -- Add a `defaultSettingValue("slack")` case: - ```go - case "slack": - return model.JSONMap{"client_id": "", "client_secret_set": false, "signing_secret_set": false} - ``` -- Register encrypted fields in `secretKeysBySection` (line ~281): - ```go - "slack": {"client_secret", "signing_secret"}, - ``` -- Add a merge/encrypt branch in `UpdateSetting` for `key == "slack"` that - encrypts `client_secret` / `signing_secret` via `crypto.EncryptOrPlain` and - sets the `*_set` booleans (copy the `github_app` / `email` branch pattern). - -### 3.3 Slack OAuth provider - -**New file:** `apps/api/internal/oauth/slack.go` — implement the same interface -as `oauth/github.go` (`Name`, `AuthURL`, `Exchange`, `GetUserInfo` where -relevant). For Slack use the **v2** OAuth endpoints: -- Authorize: `https://slack.com/oauth/v2/authorize` -- Token exchange: `https://slack.com/api/oauth.v2.access` -- Bot scopes: `chat:write`, `channels:read`, `groups:read` (and - `channels:join` if auto-joining public channels). -- The token response returns `access_token` (bot token, `xoxb-…`), `team.id`, - `team.name`, `bot_user_id`, `scope` — capture these into `TokenData` + - workspace-integration metadata. - -> Slack's OAuth is workspace-install-oriented (not "login as user"), so unlike -> the GitHub *login* provider this is only used for the **install** flow, not -> auth. Keep the provider minimal. - -### 3.4 Slack client package - -**New package:** `apps/api/internal/slack/` (mirrors `internal/github/`) -- `client.go` — thin HTTP client for the Slack Web API: - - `PostMessage(ctx, token, channelID, text string, blocks any) error` → - `POST https://slack.com/api/chat.postMessage`. Slack returns HTTP 200 with a - JSON `{ "ok": false, "error": "…" }` on logical failures — **must** check - the `ok` field and return an error so the queue retry logic engages. - - `ListChannels(ctx, token string, cursor string) ([]Channel, nextCursor, error)` - → `conversations.list` (for the channel-select UI). -- `verify.go` — `VerifySignature(signingSecret, timestamp, body, header)` using - Slack's `v0=` HMAC-SHA256 scheme (mirror `github/webhook.go`'s - `VerifySignature`). Only needed if/when inbound events are added; include the - stub now for parity but it's optional for v1 (no inbound events in scope). -- `notification.go` — `BuildSlackMessage(sender string, data …) (text, blocks)` - mirroring `mail/notification.go`'s `BuildNotificationEmail`, producing Slack - Block Kit blocks (issue ref, title, actor, before/after, link). - -### 3.5 Store layer - -**New file:** `apps/api/internal/store/slack.go` — `SlackChannelLinkStore` -(pure DB, no service calls): -- `Create`, `GetByProject`, `Update`, `SoftDelete`, `ListByWorkspaceIntegration`. -- Copy the structure of `store/*` github stores. - -### 3.6 Service layer - -**New file:** `apps/api/internal/service/slack.go` — `SlackService` -(business logic; enforces workspace/project membership like `IntegrationService` -and `GithubSyncService`): -- `InstallSlack(ctx, workspaceSlug, userID, tokenData)` — creates/updates the - `workspace_integrations` row for provider `slack`, stores encrypted bot token. -- `Uninstall` path — reuse `IntegrationService.Uninstall` (already generic on - `:provider`), and **cascade**: soft-delete all `slack_channel_links` for the - workspace so delivery stops (acceptance criterion). -- `ListChannels(ctx, workspaceSlug, userID)` — proxies `slack.ListChannels` - using the stored bot token. -- `LinkChannel` / `GetChannelForProject` / `UnlinkChannel` — per-project channel - management. -- `LoadSlackAppCredsFromSettings(ctx, settings)` helper — mirrors - `service.LoadGitHubAppNameFromSettings` / `LoadGitHubWebhookSecretFromSettings`. -- Define sentinel errors (`ErrSlackNotConfigured`, `ErrSlackNotInstalled`, - `ErrChannelLinkNotFound`, …) and map them in `writeIntegrationError`. - -### 3.7 Notification fan-out hook (the delivery leg) - -**`apps/api/internal/queue/queue.go`** -- Add queue + task constants: - ```go - QueueSlack = "devlane.slack" - TaskSlackPost = "slack_post" - ``` -- Declare `QueueSlack` in `NewPublisher` (add to the queue slice + `queues` map). -- Add `type SlackPostPayload struct { WorkspaceIntegrationID, ChannelID, Text string; Blocks any; Kind string }` - and `PublishSlackPost(ctx, payload)` (mirror `PublishSendEmail`). - -**`apps/api/internal/queue/consumer.go`** -- Add `HandleSlackPost(log, poster func(ctx, token, channelID, text, blocks) error) TaskHandler` - mirroring `HandleSendEmail` (decode → resolve token → post → return err so the - existing `maxRetries = 3` republish logic applies). - -**`apps/api/internal/service/notification.go`** -- Add `s.slackQueue`/config wiring + `SetSlack…` setters next to the email ones. -- In `emit()` (after `enqueueNotificationEmails`, ~line 332), add: - ```go - if s.slackEnabled() { - s.enqueueSlackNotifications(ctx, allowed, params, actorName, issueRef) - } - ``` - Gate it exactly like email (`queue != nil && appURL != ""`) so a Slack outage - or missing RabbitMQ never blocks the user's action. -- Implement `enqueueSlackNotifications`: resolve the issue's **project → - channel link**; if the project has a linked channel and the event type is - enabled in `events`, build the message and `PublishSlackPost`. Note this is - **channel-scoped** (one post per project channel), not per-receiver — so it - runs once per issue event, not once per receiver like email. - -> Because delivery is channel-based, the natural trigger key is the issue's -> project, not the receiver set. Consider posting whenever the event fires and a -> channel is linked (independent of who the in-app receivers are), which better -> matches "team channel" semantics. Confirm this product choice before building. - -### 3.8 Wiring & routes - -**`apps/api/cmd/api/main.go`** -- Build the Slack poster (`slack.NewClient`) and register the consumer handler: - ```go - consumer.Register(queue.QueueSlack, queue.HandleSlackPost(log, slackPoster)) - consumer.Run(ctx, []string{queue.QueueEmails, queue.QueueWebhooks, queue.QueueSlack}) - ``` - -**`apps/api/internal/router/router.go`** -- Construct `slackSvc := service.NewSlackService(...)`, add it to - `IntegrationHandler` (extend the struct), wire the queue into - `notificationSvc` (a `SetSlackQueue` alongside `SetQueue`). -- Add a hot-reload hook for the `slack` settings key (mirror the `github_app` - reload at router.go ~line 219). -- Register routes (mirror the GitHub block, trailing slashes intentional): - ``` - GET /auth/slack/install?workspace=:slug (RequireAuth) → SlackInstallStart - GET /auth/slack/callback (RequireAuth) → SlackInstallCallback - GET /api/workspaces/:slug/integrations/slack/channels/ → SlackListChannels - GET /api/workspaces/:slug/projects/:projectId/integrations/slack/channel/ → SlackGetChannel - POST /api/workspaces/:slug/projects/:projectId/integrations/slack/channel/ → SlackLinkChannel - PATCH /api/workspaces/:slug/projects/:projectId/integrations/slack/channel/ → SlackUpdateChannel (event toggles) - DELETE /api/workspaces/:slug/projects/:projectId/integrations/slack/channel/ → SlackUnlinkChannel - ``` - Uninstall reuses the existing generic - `DELETE /api/workspaces/:slug/integrations/:provider/`. - -**`apps/api/internal/handler/integration.go`** (or a new `integration_slack.go`) -- Add `SlackInstallStart` / `SlackInstallCallback` mirroring - `GitHubInstallStart` / `GitHubInstallCallback`: carry workspace slug in a - state cookie, redirect back to - `//settings?section=integrations` with `?connected=slack` / `?error=…` - (reuse the `redirectIntegration` helper, generalizing the hardcoded - `connected=github`). - ---- - -## 4. Frontend (`apps/web`) - -### 4.1 Service + types -- **`src/services/integrationService.ts`** — add: - - `slackInstallUrl(workspaceSlug)` (top-level navigation, like - `githubInstallUrl`). - - `slackListChannels(workspaceSlug)` - - `slackGetProjectChannel` / `slackLinkProjectChannel` / - `slackUpdateProjectChannel` / `slackUnlinkProjectChannel` - (404 → null pattern, as `githubGetProjectSync` does). -- **`src/api/types.ts`** — add `SlackChannel`, `SlackChannelLinkResponse`, and - extend the integration provider union to include `slack`. - -### 4.2 Integrations UI -- **`src/components/integrations/IntegrationsSection.tsx`** — currently - hardcoded to `installed.find((wi) => wi.provider === 'github')`. Generalize to - render a list/registry of providers and add a **Slack card** (Connect / - Disconnect + "Connected as "). -- **New `src/components/integrations/SlackChannelSettingsModal.tsx`** — per - project: list channels the bot can post to, link/unlink, and event toggles. - Model it on `RepoSyncSettingsModal.tsx`. - -### 4.3 Instance admin -- **New `src/pages/instance-admin/InstanceAdminIntegrationSlackPage.tsx`** — - form for Slack app `client_id`, `client_secret`, `signing_secret` (write-only - secret pattern with `*_set` masks, exactly like `InstanceAdminEmailPage` / - the GitHub App page). -- Register it in `src/pages/instance-admin/index.ts` and add the lazy route + - nav entry in `src/routes/index.tsx` (mirror - `InstanceAdminIntegrationGitHubPage`, route - `instance-admin/integrations/slack`). - ---- - -## 5. Testing & validation - -- **Go unit tests** (`go test ./...`): `slack` signature verify (if included), - `BuildSlackMessage`, service membership enforcement, channel link CRUD. Mirror - `github/refparse_test.go` and `service` test patterns. -- **Queue retry**: reuse existing `consumer` behavior; add a test asserting a - failing `chat.postMessage` republishes with incremented `x-retry-count` and - discards after 3. -- **Local manual test**: connect Slack in a dev workspace, link a channel, then - assign/comment on an issue and confirm the message posts. (For a fully offline - loop, a stub Slack endpoint can stand in for `chat.postMessage`, analogous to - the Mailpit setup used for email.) -- **`npm run validate`** must pass (web typecheck + lint + prettier + go vet + - go test). -- Conventional Commits; branch + PR (don't commit to `main`); disclose AI - assistance per `CONTRIBUTING.md`. - ---- - -## 6. Acceptance criteria (from `Taks.md`) → where satisfied - -| Criterion | Satisfied by | -| --- | --- | -| Admin enters Slack app creds in instance settings | §3.2 + §4.3 | -| Workspace admin connects Slack via OAuth, sees "Connected" | §3.3 + §3.8 + §4.2 | -| Link/unlink a Slack channel per project | §3.5–3.6 + §4.2 | -| Configured events post a formatted message to the channel | §3.4 + §3.7 | -| Slack tokens never returned to client | §3.1 (`json:"-"` on token fields) | -| Slack/RabbitMQ failure never blocks the originating action | §3.7 (queue path + gating + graceful degrade) | -| Disconnecting Slack unlinks channels & stops delivery | §3.6 (cascade soft-delete) | -| Layered arch, workspace-scoped URLs w/ trailing slashes, up+down migrations, `npm run validate` | §3.8 + §3.1 + §5 | - ---- - -## 7. Suggested delivery order (PR-sized steps) - -1. **Migration + model + store** (`000007`, `model/slack.go`, `store/slack.go`) — no behavior yet. -2. **Instance settings `slack` key** (backend + admin UI) — creds can be saved. -3. **OAuth provider + install/callback handler + routes** — Connect/Disconnect works end-to-end; token stored. -4. **Channel list + per-project link/unlink** (service + routes + web modal). -5. **Queue task + consumer + `slack.PostMessage` + notification hook** — messages actually post. -6. **Polish**: event toggles, error states, tests, `npm run validate`. - -Each step is independently reviewable and leaves the app in a working state. - ---- - -## 8. Open questions to confirm before building - -1. **Trigger semantics**: post to the channel on every configured event - regardless of in-app receivers (team-channel model), or only when there's at - least one in-app receiver? (§3.7) -2. **Which events** are channel-worthy by default (all five, or a subset like - created/state-changed)? Drives the `events` jsonb defaults. -3. **Public vs private channels**: require the bot to be invited, or attempt - `channels.join` for public channels automatically? (affects OAuth scopes) -4. **Message format**: plain text vs Block Kit richness for v1. diff --git a/SLACK_INTEGRATION_PRD.md b/SLACK_INTEGRATION_PRD.md deleted file mode 100644 index 9b8563a9..00000000 --- a/SLACK_INTEGRATION_PRD.md +++ /dev/null @@ -1,246 +0,0 @@ -# PRD — Slack Integration for Channel Notifications - -| Field | Value | -| --- | --- | -| **Product** | Devlane | -| **Feature** | Slack Integration (channel notifications) | -| **Type** | Feature | -| **Priority** | Medium | -| **Area** | Integrations (API + Web) | -| **Status** | Draft | -| **Author** | — | -| **Related docs** | [`Taks.md`](./Taks.md) (task spec), [`SLACK_INTEGRATION_PLAN.md`](./SLACK_INTEGRATION_PLAN.md) (implementation plan) | - ---- - -## 1. Overview - -Devlane's issue activity (assignments, state changes, comments, mentions, field -changes) currently flows only to **in-app notifications** and **email**. Teams -coordinate in Slack, but there is no supported way to surface that activity in a -Slack channel. This feature lets a workspace connect Slack and post configured -issue events to a **project-linked channel**, so a team sees relevant updates -where they already work. - -The integration reuses Devlane's existing patterns: the **GitHub integration** -for connect/config scaffolding, and the **email notification pipeline** (async -queue with retries) for delivery. - ---- - -## 2. Problem statement - -- Activity that matters to a team (e.g. "issue moved to In Review", "someone - commented") is invisible in Slack, where the team actually coordinates. -- Users who want this today must build their own polling or webhook tooling. -- This is a gap relative to comparable tools and relative to Devlane's own - GitHub integration, which is fully productionized while Slack "exists in name - only" (a doc comment referencing `slack` with no model, OAuth, package, or UI). - ---- - -## 3. Goals & non-goals - -### Goals -- G1. A workspace admin can connect Slack to a workspace via OAuth in a few - clicks and see it as "Connected". -- G2. A project member can link one Slack channel per project and choose which - event types post there. -- G3. Configured issue events post a correctly formatted message to the linked - channel, reliably and without blocking the user's action. -- G4. Credentials and tokens are handled securely and never exposed to clients. -- G5. Disconnecting Slack cleanly stops all delivery. - -### Non-goals (v1) -- N1. Per-user DMs / mapping Devlane users to Slack users (channel-level only). -- N2. Two-way sync — creating Devlane issues from Slack, slash commands, - interactive message actions. -- N3. Threaded conversation mirroring. -- N4. Multiple channels per project or cross-project routing rules. - ---- - -## 4. Users & personas - -| Persona | Needs | Involvement | -| --- | --- | --- | -| **Instance admin** | Configure the Slack app credentials once for the whole instance | Stage 1 (setup) | -| **Workspace admin** | Connect/disconnect Slack for their workspace | Stage 2 (connect) | -| **Project member / lead** | Link a channel to a project and choose events | Stage 3 (config) | -| **Team member** | Passively receives issue updates in the shared channel | Stage 4 (consumption) | - ---- - -## 5. User stories - -- As an **instance admin**, I can enter Slack app credentials (client id, client - secret, signing secret) in instance settings so workspaces can connect. -- As a **workspace admin**, I can click "Connect" on a Slack card, approve access - in Slack, and return to see "Connected as ". -- As a **project lead**, I can pick a Slack channel for my project from a list of - channels the bot can post to, and toggle which event types are delivered. -- As a **project lead**, I can unlink a channel to stop posting for that project. -- As a **team member**, I see messages like "Priya moved ALP-42 from Todo to In - Progress" in our project channel, with a link back to the issue. -- As a **workspace admin**, I can disconnect Slack and be confident all delivery - stops immediately. - ---- - -## 6. Functional requirements - -### 6.1 Instance configuration -- FR-1. Instance admin UI provides fields for Slack `client_id`, - `client_secret`, `signing_secret`. -- FR-2. Secrets are stored encrypted in `instance_settings` (key `slack`) and - are write-only from the UI (masked, `*_set` indicators), consistent with SMTP - and GitHub App credential handling. -- FR-3. Saving new credentials takes effect without an API restart. - -### 6.2 Workspace connect (OAuth) -- FR-4. A "Connect" action starts a Slack OAuth v2 install (full-page redirect). -- FR-5. The callback verifies state (CSRF), exchanges the code for a bot token, - and stores the token + team metadata against the workspace. -- FR-6. On success the user returns to - `//settings?section=integrations` with a success indicator; on - failure, with an error message. -- FR-7. The Integrations page shows Slack as "Connected as " with a - Disconnect action, alongside the existing GitHub card. - -### 6.3 Project channel linking -- FR-8. Users can list channels the bot can post to (via Slack - `conversations.list`). -- FR-9. Users can link exactly one channel per project. -- FR-10. Users can configure which event types (assigned, state changed, - commented, mentioned, field changed) post to the channel. -- FR-11. Users can unlink the channel for a project. - -### 6.4 Notification delivery -- FR-12. When a configured issue event occurs and the project has a linked - channel with that event enabled, Devlane posts a formatted message to the - channel. -- FR-13. Delivery is asynchronous (background queue) and must never block or roll - back the originating user action. -- FR-14. Failed posts retry automatically up to 3 times, then are dropped and - logged (reusing the existing queue retry mechanism). -- FR-15. Messages include: actor, issue reference + title, the change - (e.g. before → after for state), and a link back to the issue. -- FR-16. Delivery is channel-scoped: one message per issue event per linked - channel (not one per recipient). - -### 6.5 Disconnect / teardown -- FR-17. Disconnecting Slack removes the workspace install and unlinks all - channels for that workspace; no further messages are sent. - ---- - -## 7. Experience flow (summary) - -1. **Setup (instance admin, one-time):** enter Slack app creds → stored - encrypted. -2. **Connect (workspace admin):** Connect → Slack consent → callback stores bot - token → "Connected". -3. **Configure (project lead):** pick channel + event toggles per project. -4. **Runtime (automatic):** issue event → notification fan-out → queue → Slack - `chat.postMessage` → message appears in channel. -5. **Disconnect:** removes install + channel links → delivery stops. - -_Full technical sequence for each stage is in [`SLACK_INTEGRATION_PLAN.md`](./SLACK_INTEGRATION_PLAN.md)._ - ---- - -## 8. Non-functional requirements - -- **Security:** Bot tokens and app secrets never serialized to clients - (`json:"-"`); secrets encrypted at rest; OAuth state verified to prevent CSRF. -- **Reliability:** Slack or RabbitMQ outages degrade gracefully — the user's - action always succeeds; delivery retries and fails silently (logged). -- **Consistency:** Follows the layered architecture (handler → service → store), - workspace-scoped nested URLs with trailing slashes, and instance-settings - credential resolution used elsewhere. -- **Performance:** Posting is off the request hot path (queued), so it adds no - latency to issue edits. -- **Observability:** Send attempts, successes, and failures are logged (mirroring - the mail path's `LogSendAttempt` / `LogSent` / `LogFailed`). - ---- - -## 9. Success metrics - -- **Adoption:** # of workspaces that connect Slack; # of projects with a linked - channel. -- **Delivery health:** Slack post success rate (target > 99% excluding invalid - channels); retry/drop counts stay low. -- **Engagement (proxy):** click-throughs from Slack messages back into Devlane - issues. -- **Reliability guardrail:** zero incidents of a Slack/queue failure blocking or - rolling back an issue action. - ---- - -## 10. Dependencies & assumptions - -- A registered **Slack app** (created in the Slack dashboard) providing - `client_id`, `client_secret`, `signing_secret`, and declared bot scopes - (`chat:write`, `channels:read`, `groups:read`, optionally `channels:join`). -- A **publicly reachable API URL** for Slack's OAuth redirect - (`/auth/slack/callback`) — requires a tunnel (e.g. ngrok) for local dev. -- **RabbitMQ** for the async delivery path (optional infra; feature degrades - gracefully without it — no delivery, but no errors). -- Existing notification fan-out in `service/notification.go` as the trigger - point. - ---- - -## 11. Risks & mitigations - -| Risk | Impact | Mitigation | -| --- | --- | --- | -| Slack API logical failures return HTTP 200 with `ok:false` | Silent non-delivery | Client checks `ok`; return error so retries engage | -| Bot not in target channel / private channel | Post fails | Surface clear error in channel-select UI; optionally `conversations.join` for public channels | -| OAuth redirect can't reach localhost | Blocks local testing | Document tunnel requirement; provide setup steps | -| Token leakage | Security incident | `json:"-"` on token fields; encrypted at rest; never in API responses | -| Message spam in busy projects | Channel noise | Per-event toggles; channel-scoped (not per-user); consider future rate limiting | - ---- - -## 12. Milestones (delivery order) - -1. **M1 — Foundation:** migration `000007`, model, store (no behavior). -2. **M2 — Credentials:** instance-settings `slack` key + admin UI. -3. **M3 — Connect:** OAuth provider, install/callback, routes, Connect/Disconnect UI. -4. **M4 — Channels:** list/link/unlink channel per project + event toggles UI. -5. **M5 — Delivery:** queue task, consumer, Slack client, notification hook. -6. **M6 — Hardening:** tests, error states, `npm run validate`, docs. - -Each milestone leaves the app in a working, reviewable state (one PR each). - ---- - -## 13. Acceptance criteria - -- [ ] Instance admin can enter and save Slack app credentials (secrets masked). -- [ ] Workspace admin can connect Slack via OAuth and see "Connected as ". -- [ ] A user can link and unlink a Slack channel per project. -- [ ] Configured events post a correctly formatted message to the linked channel. -- [ ] Slack access tokens are never returned to the client. -- [ ] A Slack API failure or missing RabbitMQ never blocks or rolls back the - originating Devlane action; failures are logged. -- [ ] Disconnecting Slack unlinks channels and stops delivery. -- [ ] New endpoints follow layered architecture + workspace-scoped URL/trailing- - slash conventions; migrations ship with up + down files; `npm run validate` - passes. - ---- - -## 14. Open questions - -1. **Trigger semantics** — post on every configured event when a channel is - linked (team-channel model), or only when there's at least one in-app - receiver? -2. **Default events** — which event types are enabled by default per project? -3. **Public vs private channels** — require inviting the bot, or auto-join public - channels via `channels.join`? -4. **Message richness** — plain text vs Block Kit formatting for v1. -5. **Multiple channels per project** — confirmed out of scope for v1; revisit - later? diff --git a/instance_settings_202607251917.json b/instance_settings_202607251917.json deleted file mode 100644 index a425406c..00000000 --- a/instance_settings_202607251917.json +++ /dev/null @@ -1,45 +0,0 @@ -{ -"instance_settings": [ - { - "key" : "auth", - "value" : "{\"github\": false, \"gitlab\": false, \"google\": false, \"password\": true, \"magic_code\": true, \"allow_public_signup\": true}", - "created_at" : "2026-07-10T22:00:45.960Z", - "updated_at" : "2026-07-10T22:00:45.960Z" - }, - { - "key" : "ai", - "value" : "{\"model\": \"gpt-4o-mini\", \"api_key_set\": false}", - "created_at" : "2026-07-10T22:00:45.960Z", - "updated_at" : "2026-07-10T22:00:45.960Z" - }, - { - "key" : "image", - "value" : "{\"unsplash_access_key_set\": false}", - "created_at" : "2026-07-10T22:00:45.960Z", - "updated_at" : "2026-07-10T22:00:45.960Z" - }, - { - "key" : "general", - "value" : "{\"admin_email\": \"indravihar15722@gmail.com\", \"instance_id\": \"072d575f304d529a4d624389\", \"instance_name\": \"Fractal Analytics\", \"only_admin_can_create_workspace\": false}", - "created_at" : "0001-12-30T00:00:00.000Z", - "updated_at" : "2026-07-10T22:05:54.628Z" - }, - { - "key" : "email", - "value" : "{\"host\": \"localhost\", \"port\": \"1025\", \"security\": \"None\", \"username\": \"\", \"password_set\": false, \"sender_email\": \"dev@devlane.local\"}", - "created_at" : "0001-12-30T00:00:00.000Z", - "updated_at" : "2026-07-11T14:24:52.647Z" - }, - { - "key" : "github_app", - "value" : "{\"app_id\": \"4342116\", \"app_name\": \"devlane-local-app\", \"client_id\": \"Iv23linSrDFBt33MTPkV\", \"private_key\": \"enc:8TzbGXT+r2XM\/KBT8ExZYErVyQehRdl80+TRwYmq6hvKlEgNx9oUDSybAIOrM2JIpYHPPk+dnmmQi1M3LEii0IRNqxAqwxumFODwXa9DTKfTs+0y3omojScenm1jHmW3WrgZn3aa8DJODB+v5ZlFmcSaBMRt8YOFDvf26gXVenkcMXh\/P7Wdrf3\/etOmw00cXq2fxBmYoyxDeAEkWLRxcCHBliHH4PjNcUaNaf3umfbxKEl6WUinSlbpjdJTp\/z5C\/0NzVEPcTVwvfhL1pAPxwi0ZTeZJ+mDw41hrQcx6gr1Da4nbJrFQu1w9I4it8oHA+sQBthV0rAZy3lZuYSEjAnKQ1nB3JugCJc0cyIVz0lC+O3IJgICsmIpqNmxMHSRDbCrYV6OgC1cQHXNYtURf2Iye5T0upvThuH\/WM5tzfu3GAZS\/8L4GHSxv87fmsiO30qGGpY6k6vFoPiOMPDojyUW5jt64T5yznkoMwYzSatFIQlNAULLiBVaXVQ7JXJ45gwY\/WI+dbcH2RMn2HO2syHKHeRvmFDzbed++EHyxQya3Kh6KZFF\/ibw0NS7GtDK1w8i0owIM57b42x4uUBSVAchGpR9qCa4uRy7OqTajXthIQZ3UVljOfDmiwWNZ8Z+fMKsZLueNL0LrnwKHConRhRQ3V8ojSyO8JWfFO0bxZmN+i5lnDUKWiuYcpyNQeX3v7qGll8MoOwbxOjq0SHYdewgnrRfjLMvfQ7uGODm9PqHV8MGrinvGquxfg0JS0DvTsOER14XmyZzIvSsra+h2RVzZ5DSppZ8VTpZAAnAPYv3+gnROKV2i9orRsSVsaAY10KNZoq9vDHghWoh9IutjI6bmLOniQur+Clc9rcC9lPt4A1\/5pMVpQ7x97VMUF53eGkXrsgdZXG9Hok5K1uc+JeZwY5PPzwkmjyWQWdAtppCbtJJPLjA1lvFYIn5r7niHl8ondcGpxNk7HViJKqOxTfAOSz3gLX1OXUlretLFyzZ6Ll+dyPLP14nP3zgNfLgmIfDtpahiRKT1WLQwY2CjilBJJxIS0k+mb9WVU17JP6cB682\/w3x00oT7V0AUOBDXIvekFGn2MfAfokcxzwgzQw31tyeYUkxqW+Pa3I3zTuPD48Z0+sXDtmYW3AT4wqqAxg4DRvluG9kwWK3jHcTbUKd0tmlf9+Zi7YtIbrER89Gh0culi\/jSIWhCkjTWfApb24TASzMrZewf9D1UC9hYtMpuJ8GI4l0u5QgG0B0GAkdTD33F+Qrq0DTeGFcxY1kuuNXAcxYT1IsKTaPYrFVTQuNBZaLtt4Iwkiy0MCf4hAmr84BXNrg+RYc+e5YapLJWtFl\/i4udouTJnylnezXZmnjxCHwXaYfGRMqVIsUqE0qmKn4JwNn1bwMTgBLY9bzc9kQN0ZAcP0TBPzLu4lix7GrNlJDhGEYhLqNPRXSQrG6FahaqSgsPAUHbK7mZAkhHFTxpoNUxRmFzSIXK706Z73BAOsMGFkVYmq7y0hhNo2oipx8ZNH8OuMzX5lwnHimhHvYK+yKdLEPp7V2wlDRm8EUqy5VjPvgnh83Y37ukkyH1ZNLyL+MNMBsd925MG2Hf3k+eT4b06gAmiUGiLfZgJn3n5Eq4btvTtySek8bdj6b3WiPA6V+LKmE7\/FbsrLfB3eIOQ24HcdIGnzPDxXVt5J2A0uLFQKBEWufoTFRy3xi0cOZEXrxKamodUf9LRORQuTl50r1xbc4RDVGt6b43ioNy\/zE1H0tW1ET4kytzpPcFhrpHmfKA9IubEwvgHIwoVl86uLR2nVBFOsNAhr6L9HkmT2nTFtimTBeLuzxfprDHJKVMZ+S7K486kPFjMak7HeKu9R\/+8s5q5b01WTgfPUnHYPBoJXD6+ssJwiaZcOS7QnE4MgbetaKpLh8ayZlZRVlJRL19ePscu7xWlkoVa62CN6wGOnTmC5FCsi0jcOyvDfrPhaBbO5GOp+fT4RAFzPeY9TxUksL+jA3UYtt+mP9kLunWIF1lcj7jIl9MJmUhItQZaJ8pmbL2LyScG8HWKW3Va0FV8vInz0vB0lD+cs401NGqdJecquFto7mr6Lh2jZHMmW+SqKOWzuuM25vm+EM2RRYaO44YXKG\/Qumo5BRR\/YcluGDx6PwHrN4eGC9eZvthkAS6Sut7me5TTUIK5AX8u6rmegnhoEWilE+dnXY5funtdKQG\/CtAxHwLF5J9JOCZZzwXVh1aQ3R47CU7Pvj8+OEu6aUsXxK7doou5eg0TKy5Lw=\", \"client_secret\": \"enc:h5BG6yrS747Bz5eT7PAmqC60GVQJSGMR+d7v7AvU2R0kWzLq2rGXoYsHslOmyPQuZTTE\/81FqmKMaEvtsFLOIjnxeXA=\", \"webhook_secret\": \"enc:qI2W5n5yX4HRsaERa0yeff3pEXphb2v0WM9Zuwt6l8uWll83qMM=\", \"private_key_set\": true, \"client_secret_set\": true, \"webhook_secret_set\": true}", - "created_at" : "0001-12-30T00:00:00.000Z", - "updated_at" : "2026-07-20T00:25:55.845Z" - }, - { - "key" : "slack_app", - "value" : "{\"client_id\": \"adfadsf\", \"client_secret\": \"enc:0OfeT2Psqj0\/XDah3v+0NK3Wo4qenbP3hB06ZxWy2h7pjQ==\", \"signing_secret\": \"enc:6lVQruceFNWNuYkNnhb7togJHZajr\/NLutSm50byAQaXA94Q\", \"client_secret_set\": true, \"signing_secret_set\": true}", - "created_at" : "0001-12-30T00:00:00.000Z", - "updated_at" : "2026-07-25T08:41:23.530Z" - } -]} diff --git a/smee.log b/smee.log deleted file mode 100644 index 34c56c18..00000000 --- a/smee.log +++ /dev/null @@ -1,12 +0,0 @@ -Connected to https://smee.io/xyz123 -Forwarding https://smee.io/xyz123 to http://localhost:8080/webhooks/github -POST http://localhost:8080/webhooks/github - 200 -POST http://localhost:8080/webhooks/github - 200 -POST http://localhost:8080/webhooks/github - 200 -POST http://localhost:8080/webhooks/github - 200 -POST http://localhost:8080/webhooks/github - 200 -POST http://localhost:8080/webhooks/github - 200 -POST http://localhost:8080/webhooks/github - 200 -POST http://localhost:8080/webhooks/github - 200 -POST http://localhost:8080/webhooks/github - 200 -POST http://localhost:8080/webhooks/github - 200 From 1d25fd2dbdfa54f429549ce20992fceb55138ac3 Mon Sep 17 00:00:00 2001 From: Amit kumar Date: Tue, 28 Jul 2026 23:42:06 +0530 Subject: [PATCH 4/4] chore: delete Task.md and Todo.md --- Task.md | 131 -------------------------------------------------------- Todo.md | 6 --- 2 files changed, 137 deletions(-) delete mode 100644 Task.md delete mode 100644 Todo.md diff --git a/Task.md b/Task.md deleted file mode 100644 index 27ef2c7a..00000000 --- a/Task.md +++ /dev/null @@ -1,131 +0,0 @@ -# Slack Integration for Channel Notifications - -| Field | Value | -| --- | --- | -| **Type** | Feature | -| **Priority** | Medium | -| **Area** | Integrations (API + Web) | -| **Status** | Proposed | - -## Summary - -Devlane has no Slack integration. Teams cannot post issue/project notifications to -Slack channels, so activity that already flows through Devlane's in-app notification -system (assignments, state changes, comments, etc.) has no path into the chat tools -where teams actually coordinate. GitHub is the only integration that is fully wired -end-to-end; Slack exists in name only. - -## Current State (Evidence) - -- **Model:** `apps/api/internal/model/integration.go` references `slack` only in a - doc comment on the `Integration` struct (`"a registered integration provider - (github, slack, ...)"`). There is no Slack-specific model, no per-project channel - table, and no token storage. -- **OAuth:** `apps/api/internal/oauth/` contains providers for `google`, `github`, - and `gitlab` only (`google.go`, `github.go`, `gitlab.go`). There is no Slack OAuth - provider and no Slack app install/callback flow. -- **Provider package:** `apps/api/internal/github/` implements a complete provider - (client, app, installations, webhook, ref parsing). There is no equivalent - `slack/` package. -- **Handlers:** `apps/api/internal/handler/integration.go` exposes generic - integration endpoints plus GitHub-specific install/sync/webhook flows. No Slack - handler or routes exist. -- **Services:** `apps/api/internal/service/` has `integration.go`, `github_sync.go`, - and `github_events.go`, plus `notification.go` (which fans out in-app/email - notifications via `Emit*` methods and the RabbitMQ `queue.Publisher`). Nothing - sends to Slack. -- **Instance settings:** `apps/api/internal/model/instance_setting.go` is the - key-value (JSONB) store used for SMTP and OAuth provider credentials, resolved at - request time. There is no Slack app credential key. -- **Web UI:** `apps/web/src/components/integrations/` contains only GitHub - components (`IntegrationsSection.tsx`, `RepoSyncSettingsModal.tsx`). - `IntegrationsSection` is hardcoded to a single `github` provider (`installed.find( - (wi) => wi.provider === 'github')`) with no Slack card, connect button, or - channel-select UI. - -**GitHub integration is complete** and is the reference pattern to follow for -layering, routing conventions, and instance-settings-based credential resolution. - -## Problem - -There is no supported way to deliver Devlane notifications to Slack. Users who want -issue activity surfaced in a Slack channel must build their own polling or webhook -tooling. This is a gap relative to comparable tools and relative to Devlane's own -GitHub integration, which is fully productionized. - -## Proposed Scope - -Deliver Slack notifications following the existing GitHub integration patterns -(handler → service → store layering, workspace-scoped nested URLs with trailing -slashes, instance-settings credential resolution, graceful degradation when optional -infra is absent). - -1. **Data model + migration** (`apps/api/internal/model/`, `apps/api/migrations/`): - - A per-project Slack channel/token model (workspace + project scoped) holding the - Slack team/workspace id, channel id + name, and the bot/OAuth access token - (stored in a way consistent with how existing secrets/credentials are handled — - tokens must never be serialized back to the client, matching the - `WebhookSecret` / `Credentials` `json:"-"` pattern). - - Reuse `integrations` / `workspace_integrations` where it fits (register `slack` - as a provider row; store the install under `workspace_integrations`), mirroring - GitHub. - - Add both `NNNNNN_.up.sql` and `.down.sql`; never edit merged migrations. - -2. **Slack OAuth + install handler** (`apps/api/internal/oauth/slack.go`, - `apps/api/internal/slack/`, `apps/api/internal/handler/`): - - Slack OAuth provider and an install/callback flow that mirrors the GitHub - install handler, redirecting back to - `//settings?section=integrations` with `?connected=slack` / `?error=...`. - - App credentials (client id/secret, signing secret) resolved from - `instance_settings` at request time, not env. - -3. **Notification sender** (`apps/api/internal/slack/` + `apps/api/internal/service/`): - - A Slack client that posts messages to a channel (`chat.postMessage`). - - Hook into the notification fan-out so relevant events (config-driven per - project) are delivered to the linked channel. Prefer routing through the - existing background `queue.Publisher` path (as email notifications do) so a - Slack outage can't block or roll back the user's action; degrade gracefully if - RabbitMQ is absent. - -4. **Web UI** (`apps/web/src/components/integrations/`, - `apps/web/src/services/integrationService.ts`, `apps/web/src/api/types.ts`): - - Add a Slack provider card to `IntegrationsSection` (Connect / Disconnect), - generalizing the currently GitHub-only lookup. - - A per-project channel-select UI (list channels the app can post to; link/unlink - a channel per project), following the linked-repositories panel pattern. - - New service methods on `integrationService` (install URL, list channels, link / - unlink channel), calling through `apiClient`. - -5. **Instance admin settings** - (`apps/web/src/pages/instance-admin/`, `instance_settings`): - - Admin UI + backend key to store Slack app credentials, matching how SMTP/OAuth - provider creds are configured today. - -## Out of Scope (initial) - -- Two-way sync (creating Devlane issues from Slack, slash commands, interactive - message actions). -- Slack DMs / per-user notification routing (channel-level only for v1). -- Threaded conversation mirroring. - -## Acceptance Criteria - -- An instance admin can enter Slack app credentials in instance settings. -- A workspace admin can connect Slack via OAuth and see it as "Connected" alongside - GitHub in workspace settings → Integrations. -- A user can select/link a Slack channel per project and unlink it. -- Configured notification events post a correctly formatted message to the linked - channel. -- Slack access tokens are never returned to the client. -- A Slack API failure (or missing RabbitMQ) never blocks or rolls back the - originating Devlane action; failures are logged. -- Disconnecting Slack unlinks channels and stops delivery. -- New endpoints follow the layered architecture and workspace-scoped URL/trailing- - slash conventions; migrations ship with up + down files; `npm run validate` passes. - -## Touched Areas (per repo conventions) - -`model/` → migration → `store/` → `service/` → `handler/` → register in -`router/router.go` → `oauth/slack.go` + new `slack/` package → web -`services/integrationService.ts` → `components/integrations/` → instance-admin UI + -`instance_settings` key. diff --git a/Todo.md b/Todo.md deleted file mode 100644 index 96f27ef5..00000000 --- a/Todo.md +++ /dev/null @@ -1,6 +0,0 @@ -1. Instance setting configuration - Done -2. Database Migration: Need a new table `slack_channel_links` - Done -3. Go Models & Store: Appended Slack models to `model/integration.go` and `store/integration.go` - Done -4. Slack OAuth & Install Handler: Create `oauth/slack.go` and update `handler/integration.go` so users can authenticate with Slack - Done -5. Slack Notification Logic: Build a Slack client and wire it up with the background queue (RabbitMQ) to send channel messages - Done -6. Web UI for Channels: Add Slack to `IntegrationsSection.tsx` and create `SlackChannelSettingsModal.tsx` for linking projects to channels - Done \ No newline at end of file