Skip to content

feat: post review requests to slack with approve and reject buttons (CM-1841) - #4919

Merged
ulemons merged 4 commits into
mainfrom
feat/CM-1841-review-channel
Oct 13, 2026
Merged

ulemons merged 4 commits into
mainfrom
feat/CM-1841-review-channel

Conversation

@ulemons

@ulemons ulemons commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 6 of the onboarding-core series (after #4908, #4909, #4910, #4913, #4914, #4916). Rows routed to review by the evaluation worker now land in a Slack channel as a card with Approve / Reject buttons, and the decision is recorded on the projectCatalog row.

@crowd/slack

  • New reviewRequest.ts: buildReviewRequestBlocks(card) (Block Kit: repo, project name, origin link, classification kind, proposed action, two buttons with a confirm dialog, value = catalog id), buildReviewDecisionMessage(card, decision, reviewedBy) (same summary, buttons replaced by the outcome) and postReviewRequest(card) (bot token, posts to the review channel).
  • getSlackBotConfig() also reads CROWD_SLACK_REVIEW_CHANNEL_ID.
  • Action ids approve_review / reject_review live here so worker and backend share them.

Evaluation worker

  • classifyAndRouteProjects collects rows routed to review that have no reviewMessageTs yet (reviewRequests), so a retried batch or a row routed by a previous attempt is requested once.
  • New activity notifyReviewRequest(project): posts the card and stores the message ts with setProjectCatalogReviewMessageTs. Runs with the notify proxy (1 attempt, no retry, same reasoning as CM-1791); a failure is logged and the row stays in review, so the next run requests it again.

Backend

  • interactivity.ts now handles approve_review / reject_review alongside force_onboarding; the TODO(CM-1791) warning branch is gone. The handler is a factory (createInteractivityHandler(temporal)), mounted with the Temporal client like the events route.
  • New reviewDecisionCommand.ts: validates the catalog id, resolveProjectCatalogReview (review → onboard / review → skip, guarded on action = 'review'), then replaces the card through response_url with the decision. On approve it starts onboardProjects for that id right away (onboard-review-<catalogId>, REJECT_DUPLICATE, already-started tolerated); if Temporal is not configured the row waits for the 08:00 schedule.
  • postToResponseUrl accepts the decision message type next to the block-builder DTO.

Not in scope

  • /crowd onboard-project, force onboarding, runPrecheck.ts.
  • Thread replies in the bot channel (PR 8).

Deployment notes

Test plan

  • pnpm tsc-check, pnpm lint --deny-warnings --format=agent
  • services/libs/slack: block builder, decision message, postReviewRequest without channel / with channel
  • projects_evaluation_worker: reviewRequests populated for new review rows, not for rows with a message, populated for previous-attempt rows without a message
  • backend: interactivity dispatch for both buttons, failure replacement, missing user, unknown action; review decision command (approve starts workflow, reject does not, already-started tolerated, no Temporal, row no longer in review, invalid id)
  • Staging: evaluate a known LF repo via the bot (expect card in the review channel), click Approve (expect card replaced and onboardProjects started), click Reject on another (expect skip, skipReason = rejected in review by <user>)

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 12, 2026 17:11
@cursor

cursor Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes project-catalog lifecycle and can start onboardProjects from Slack clicks; deployment depends on CROWD_SLACK_REVIEW_CHANNEL_ID and coordinating with follow-up onboarding behavior in prod.

Overview
Adds a Slack human-review loop for projectCatalog rows in review: the evaluation worker posts Block Kit cards (Approve/Reject) to a configured review channel, and button clicks record the decision and update the message.

Worker & data layer: After classification, the workflow fetches rows in review with no reviewMessageTs, posts via postReviewRequest, and persists the message ts inside a transaction with lockProjectCatalogReviewRequest so duplicate cards are avoided. setProjectCatalogReviewMessageTs now only sets when still null.

@crowd/slack: New reviewRequest helpers (card blocks, decision replacement message, postReviewRequest), shared action ids, escapeSlackText in mrkdwn, and CROWD_SLACK_REVIEW_CHANNEL_ID on bot config.

Backend: Slack interactivity is a createInteractivityHandler(temporal) factory wired with the Temporal client; approve_review / reject_review call runReviewDecisionCommand, which validates the catalog id, runs resolveProjectCatalogReview (review → onboard or skip), replaces the card via response_url, and on approve starts onboardProjects immediately (or leaves the row for the schedule if Temporal is missing or workflow start fails).

Reviewed by Cursor Bugbot for commit 4c89ecb. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Failed notifications can strand review rows, and approval failures can produce inaccurate Slack outcomes.

3 open findings
What changed in this PR

Adds Slack-based human review for project-catalog onboarding decisions, connecting the evaluation worker, shared Slack library, backend interactivity, and Temporal onboarding.

Changes:

  • Posts review cards with Approve/Reject actions.
  • Records decisions and dispatches approved onboarding.
  • Adds routing and interaction tests.
File Description
services/​libs/​slack/​src/​types.ts Adds review-channel configuration.
services/​libs/​slack/​src/​reviewRequest.ts Builds and posts review cards.
services/​libs/​slack/​src/​reviewRequest.test.ts Tests review messages and posting.
services/​libs/​slack/​src/​index.ts Exports review utilities.
services/​libs/​slack/​src/​botConfig.ts Reads the review-channel environment variable.
services/​apps/​projects_evaluation_worker/​src/​workflows/​evaluateProjects.ts Dispatches review notifications.
services/​apps/​projects_evaluation_worker/​src/​types.ts Adds review requests to routing results.
services/​apps/​projects_evaluation_worker/​src/​activities/​classifyAndRoute.ts Collects unnotified review rows.
services/​apps/​projects_evaluation_worker/​src/​activities/​classifyAndRoute.test.ts Tests review-request collection.
services/​apps/​projects_evaluation_worker/​src/​activities/​activities.ts Posts cards and stores message timestamps.
backend/​src/​services/​slack/​slackActionIds.ts Shares review action identifiers.
backend/​src/​services/​slack/​reviewDecisionCommand.ts Records and executes decisions.
backend/​src/​services/​slack/​reviewDecisionCommand.test.ts Tests review-decision handling.
backend/​src/​services/​slack/​onboardProjectCommand.ts Supports decision response messages.
backend/​src/​api/​slack/​interactivity.ts Dispatches review button actions.
backend/​src/​api/​slack/​interactivity.test.ts Tests review-action dispatch.
backend/​src/​api/​slack/​index.ts Injects Temporal into the handler.
backend/​src/​api/​index.ts Supplies the Temporal client.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread services/apps/projects_evaluation_worker/src/activities/activities.ts Outdated
Comment thread backend/src/services/slack/reviewDecisionCommand.ts Outdated
Comment thread services/libs/slack/src/reviewRequest.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 77087c9. Configure here.

Copilot AI balanced review requested due to automatic review settings October 12, 2026 17:27
@ulemons

ulemons commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Review fixes in 163fbaa:

  • Failed review posts never retried (activities.ts / evaluateProjects.ts): the workflow now calls a new fetchReviewRequests activity backed by findProjectCatalogReviewWithoutMessage (action = 'review' AND "reviewMessageTs" IS NULL), so a row whose Slack post failed is picked up again on the next run (or by the bot run for its own ids). reviewRequests was dropped from the classify result. setProjectCatalogReviewMessageTs is now also guarded on "reviewMessageTs" IS NULL and the activity warns when 0 rows were updated.
  • Temporal start failure after the row is already onboard (reviewDecisionCommand.ts): startApprovedOnboarding is best-effort, logs the error and returns whether the workflow started; the row waits for the 08:00 schedule.
  • "onboarding started" wording (reviewRequest.ts): buildReviewDecisionMessage takes { onboardingStarted } and says "queued for the next onboarding run" when Temporal is missing or the start failed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Duplicate review cards, unsafe Slack markup interpolation, and workflow-ID reuse can produce incorrect operational behavior.

0 open findings

3 resolved since last review
Previously missed (3)

In code that hasn't changed since last review

Medium severity Allow onboarding workflows to restart for later review cycles

backend/​src/​services/​slack/​reviewDecisionCommand.ts:54

Using a permanent workflow ID with REJECT_DUPLICATE prevents immediate onboarding on a later review cycle for the same catalog row. Rows that fail onboarding can be re-queued and classified for review again, but the completed workflow from the first approval makes this start throw; the catch then reports onboarding started even though no new run exists. Allow reuse after a closed execution (duplicate clicks are already rejected by the guarded review update), or include the review-cycle identity in the workflow ID.

Medium severity Prevent duplicate Slack cards with atomic notification claiming

services/​apps/​projects_evaluation_worker/​src/​activities/​activities.ts:260

This activity can create multiple actionable cards for one catalog row. Posting to Slack happens before the guarded timestamp write, so a process failure after the post leaves the row eligible for the next run; overlapping scheduled and ad-hoc evaluation workflows can also both read the null timestamp and post before either write wins. The maximumAttempts: 1 setting only prevents an activity retry and does not prevent these later fetches. Claim the notification atomically before the external side effect (with a recoverable lease/outbox state), then finalize it with the Slack timestamp.

Medium severity Escape untrusted project names before rendering Slack mrkdwn

services/​libs/​slack/​src/​reviewRequest.ts:51

projectName is accepted from API callers and is inserted directly into a mrkdwn block. A value such as <!channel> or <@U123> will be interpreted by Slack and can trigger unwanted mentions in the review channel. Escape &, <, and > before rendering this and other untrusted card text; the existing classification-alert path already applies this escaping in services/libs/project-onboarding/src/requestClassificationAlert.ts:29-30.

🧠 Review effort: Balanced

…CM-1841)

Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
…d onboarding start (CM-1841)

Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Copilot AI balanced review requested due to automatic review settings October 13, 2026 07:17
@ulemons
ulemons force-pushed the feat/CM-1841-review-channel branch from 163fbaa to 69aae5f Compare October 13, 2026 07:17
@ulemons ulemons self-assigned this Oct 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Concurrent notification duplication, unsafe Slack markup, and workflow-ID reuse can produce incorrect review behavior.

3 open findings

🧠 Review effort: Balanced

Comment thread backend/src/services/slack/reviewDecisionCommand.ts Outdated
Comment on lines +189 to +190
WHERE action = 'review' AND "reviewMessageTs" IS NULL
${ids !== undefined ? 'AND id = ANY($(ids)::uuid[])' : ''}
Comment thread services/libs/slack/src/reviewRequest.ts Outdated
…l workflow id (CM-1841)

Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
@ulemons

ulemons commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Review fixes:

  • Duplicate review cards (projectCatalog.ts, activities.ts): notifyReviewRequest now runs in a writer transaction that takes SELECT … FOR UPDATE on the row (lockProjectCatalogReviewRequest, only when action = 'review' AND reviewMessageTs IS NULL) before posting; a concurrent run blocks, re-reads the stamped ts and skips. A failed post leaves the row unclaimed for the next run.
  • Unescaped mrkdwn (reviewRequest.ts): escapeSlackText moved to @crowd/slack and applied to repo name, repo url, project name, the non-Slack origin label and the confirm dialogs; regression test with <!channel>.
  • Workflow id reuse (reviewDecisionCommand.ts): the id is now onboard-review-<catalogId>-<reviewedAt ms>, so a row re-queued after an error and approved again starts a new workflow; the already-started branch only covers a same-cycle retry.

Copilot AI balanced review requested due to automatic review settings October 13, 2026 07:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Transient failures can strand review rows, while non-atomic Slack posting can create duplicate cards.

2 open findings
2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Non-atomic Slack posting can create duplicate review cards

services/​apps/​projects_evaluation_worker/​src/​activities/​activities.ts:274

The row lock cannot make the Slack side effect and the database write atomic. If Slack accepts the post but the request times out, the activity is cancelled, or the transaction later fails, reviewMessageTs remains null and the next run posts a duplicate card. Use a Slack-side idempotency key or a durable posting/outbox state with reconciliation rather than relying on a transaction around the network call.

🧠 Review effort: Balanced

Comment thread backend/src/services/slack/reviewDecisionCommand.ts Outdated
…y (CM-1841)

Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
@ulemons

ulemons commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the transient-failure comment: an unexpected error while recording the decision now posts an ephemeral "please try again" message via the response url without replace_original, so the card with Approve/Reject stays in place (postReviewDecisionFailure). Test added.

Copilot AI balanced review requested due to automatic review settings October 13, 2026 07:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Stale and concurrent actions can produce incorrect Slack outcomes, while notification delivery is not reliably idempotent.

2 open findings
1 resolved since last review
Previously missed (3)

In code that hasn't changed since last review

Medium severity Prevent losing clicks from overwriting the final decision

backend/​src/​services/​slack/​reviewDecisionCommand.ts:107

A concurrent second click reaches this branch after the first reviewer changes the row, then replaces the same original card with the generic message. If that response arrives last, it erases the winning Approved/Rejected outcome. Return a non-replacing/ephemeral response for the losing click, or rebuild the recorded decision instead of replacing the card.

Medium severity Make Slack side effects idempotent across transaction failures

services/​apps/​projects_evaluation_worker/​src/​activities/​activities.ts:273

The external Slack side effect happens before the database transaction commits. If the timestamp update or commit fails after Slack accepts the message, reviewMessageTs remains null and the next run posts a duplicate, so the activity is not idempotent despite the once-only routing goal. Use a durable outbox/reconciliation mechanism or an externally idempotent request key rather than relying only on the row lock.

Medium severity Encode sourceUrl before constructing Slack links

services/​libs/​slack/​src/​reviewRequest.ts:36

sourceUrl is interpolated into Slack's <url|label> grammar without encoding the destination. A URL containing <, >, or | can terminate or corrupt the link (the new test even supplies angle brackets but expects them raw). Validate and encode the destination before constructing the mrkdwn link, and update the escaping test accordingly.

🧠 Review effort: Balanced

{
type: 'button',
action_id: APPROVE_REVIEW_ACTION_ID,
value: card.id,
@ulemons
ulemons merged commit 87b3d9d into main Oct 13, 2026
14 checks passed
@ulemons
ulemons deleted the feat/CM-1841-review-channel branch October 13, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants