Skip to content

AMP-153 Slack: secure and reliable /linear backlog query#98

Open
gabiIsCoding wants to merge 22 commits into
mainfrom
amp-153-slack-linear-backlog
Open

AMP-153 Slack: secure and reliable /linear backlog query#98
gabiIsCoding wants to merge 22 commits into
mainfrom
amp-153-slack-linear-backlog

Conversation

@gabiIsCoding

@gabiIsCoding gabiIsCoding commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a deterministic, read-only Slack @OpenTag linear / /linear command that lists unfinished issues without creating an Agent Run or enabling Linear mutations.
  • Require an exact Slack (teamId, channelId) allowlist and route each authorized channel to its configured Linear project.
  • Separate backlog-query credentials from apply credentials, including OAuth query-only mode, while still resolving refreshed OAuth tokens for each query.
  • ACK Slack Events API deliveries immediately through a bounded asynchronous executor and deduplicate retries by event_id.
  • Paginate all unfinished Linear issues before applying the final state/priority/identifier sort; fail safely on missing projects, malformed cursors, timeout, repeated cursors, or the 100-page safety limit.
  • Document the channel mapping/query credential configuration and add regressions for authorized, unbound, unauthorized, retry, pagination, missing-project, and query-only mutation-isolation paths.

Validation

  • pnpm vitest run packages/cli/test/start.test.ts packages/linear/test/backlog.test.ts packages/cli/test/slack-linear-backlog.test.ts — 3 files, 87 tests passed
  • pnpm typecheck — passed
  • pnpm lint — passed
  • pnpm vitest run — 124 files, 1654 tests passed
  • git diff --check — passed

Notes

  • Supersedes closed PR AMP-153 Slack: query unfinished Linear issues via /linear #97 and addresses all six reviewer blockers.
  • The /linear path remains query-only: no Agent Run, LLM invocation, apply plan, or Linear mutation is created.
  • Pagination is bounded at 100 pages / 10,000 issues and uses one overall 10-second query budget; it returns a safe error instead of a partial, incorrectly sorted backlog.

Summary by CodeRabbit

  • New Features
    • Added a read-only Slack /linear self-service command to display unfinished Linear backlog items.
    • Introduced channel-based allowlisting and query-only credential support for /linear backlog lookups.
    • Slack events now acknowledge quickly and process work asynchronously with bounded concurrency.
    • Added duplicate event deduplication and queue backpressure (503) handling.
  • Bug Fixes
    • Improved Linear configuration diagnostics and strengthened credential safety/redaction.
    • Preserved mrkdwn formatting for Linear links and safely escaped backlog content.
  • Documentation
    • Updated Slack/Linear configuration docs for authorization, credentials, and /linear behavior.

gabiIsCoding and others added 21 commits July 16, 2026 14:32
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fig for AMP-153

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n for AMP-153

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…for AMP-153

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… AMP-153

Slack self-service replies were run through markdownToSlackMrkdwn even
when the text was already hand-built Slack mrkdwn, so the escaping pass
turned <url|AMP-153> into &lt;url|AMP-153&gt; and Slack rendered the raw
markup instead of a link. Add an optional textFormat: "mrkdwn" passthrough
on SlackSelfServiceReply/reply() and have the Linear backlog handler
declare its rendered text as already-mrkdwn, skipping re-escaping without
switching the renderer to Markdown-link syntax (which would let
Linear-controlled titles inject links via the converter).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… AMP-153

Linear API rejects the query when the same ID variable is used in positions
with different type expectations: project(id: String!) and filter comparison
(ID). Declare two variables ($projectId: ID!, $projectKey: String!) and pass
the same value to both positions to satisfy schema requirements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3745dae-9baa-4fdc-b2b9-922640d521d8

📥 Commits

Reviewing files that changed from the base of the PR and between d47f412 and 8c0607b.

📒 Files selected for processing (7)
  • apps/slack-events/test/app.test.ts
  • packages/cli/src/slack-linear-backlog.ts
  • packages/cli/test/slack-linear-backlog.test.ts
  • packages/linear/src/backlog.ts
  • packages/linear/test/backlog.test.ts
  • packages/slack/src/ingress.ts
  • packages/slack/test/ingress.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/linear/src/backlog.ts
  • packages/linear/test/backlog.test.ts
  • packages/cli/test/slack-linear-backlog.test.ts
  • packages/cli/src/slack-linear-backlog.ts
  • packages/slack/test/ingress.test.ts
  • packages/slack/src/ingress.ts

📝 Walkthrough

Walkthrough

Adds read-only Slack /linear backlog queries backed by paginated Linear GraphQL retrieval, channel allowlists, query-only credentials, and startup diagnostics. Slack Events API callbacks now acknowledge asynchronously with bounded concurrency, backpressure handling, and event deduplication.

Changes

Linear backlog and Slack ingress

Layer / File(s) Summary
Linear backlog retrieval contract and pagination
packages/linear/src/backlog.ts, packages/linear/src/index.ts, packages/linear/test/backlog.test.ts
Adds paginated unfinished-issue retrieval, normalization, deterministic sorting, timeout and pagination validation, public exports, and tests.
Linear channel authorization and configuration diagnostics
packages/cli/src/config.ts, packages/cli/src/linear-backlog-config.ts, packages/cli/src/doctor.ts, packages/cli/test/*, docs/configuration.md
Adds channel mappings, query-only connections, token precedence, duplicate validation, unsupported-connection diagnostics, and configuration documentation.
Slack /linear rendering and command execution
packages/cli/src/slack-linear-backlog.ts, packages/slack/src/events.ts, packages/slack/src/dispatcher-events.ts, packages/slack/src/render.ts, packages/cli/test/slack-linear-backlog.test.ts, packages/slack/test/*, docs/platforms/slack.*
Adds escaped backlog replies, /linear routing with binding: null, mrkdwn propagation, bounded event deduplication, and Slack setup documentation.
Query-only startup and ingress wiring
packages/cli/src/start.ts, packages/cli/test/start.test.ts
Keeps query-only Linear credentials out of dispatcher mutation inputs while wiring backlog handlers and token providers into Slack ingress.
Asynchronous Slack acknowledgements and deduplication
packages/slack/src/ingress.ts, packages/slack/test/ingress.test.ts, apps/slack-events/test/app.test.ts
Adds immediate acknowledgements, bounded async processing, queue-full 503 responses, synchronous verification handling, and test flushing for detached side effects.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Slack
  participant SlackEventsIngress
  participant SlackEventProcessor
  participant LinearBacklogHandler
  participant LinearGraphQL
  Slack->>SlackEventsIngress: send event_callback
  SlackEventsIngress-->>Slack: return { ok: true }
  SlackEventsIngress->>SlackEventProcessor: process asynchronously
  SlackEventProcessor->>LinearBacklogHandler: handle /linear
  LinearBacklogHandler->>LinearGraphQL: fetch paginated backlog
  LinearGraphQL-->>LinearBacklogHandler: return issues
  LinearBacklogHandler-->>SlackEventProcessor: return mrkdwn reply
  SlackEventProcessor-->>Slack: post source-thread reply
Loading

Possibly related PRs

Suggested reviewers: mingyooagi

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, specific, and accurately summarizes the main change: a secure, reliable Slack /linear backlog query.
Description check ✅ Passed The description matches the required template with Summary, Validation, and Notes sections and includes the needed implementation and test details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch amp-153-slack-linear-backlog

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a read-only /linear Slack command that allows users to list unfinished Linear issues directly in Slack. It adds a dedicated channel allowlist and project router configuration, implements the backlog query logic via Linear's GraphQL API, and introduces a bounded asynchronous executor for Slack Events API callbacks to prevent timeouts. The review feedback highlights two valuable improvements: optimizing the backlog rendering loop by pre-calculating issue counts per state name to avoid O(N * M) complexity, and adding defensive null-safety guards when parsing the GraphQL response to prevent runtime crashes on unexpected API structures.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +81 to +89
for (const group of groupShownIssues(shown)) {
const totalInGroup = backlog.issues.filter((issue) => issue.stateName === group.stateName).length;
const shownInGroup = group.issues.length;
const countLabel = shownInGroup < totalInGroup ? `${shownInGroup} of ${totalInGroup}` : String(totalInGroup);
const emoji = STATE_TYPE_EMOJI[group.stateType] ?? "▫️";
lines.push("");
lines.push(`${emoji} *${escapeSlackText(group.stateName)} (${countLabel})*`);
for (const issue of group.issues) lines.push(renderIssueLine(issue));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Performance Optimization

Currently, backlog.issues.filter is called inside a loop for each group shown. If the backlog contains a large number of issues (up to 10,000 based on the 100-page safety limit), this results in $O(N \times M)$ complexity where $N$ is the total number of issues and $M$ is the number of groups shown (up to 20).

We can optimize this to $O(N + M)$ by pre-calculating the total counts per state name using a single-pass Map before entering the loop.

    const totalByState = new Map<string, number>();
    for (const issue of backlog.issues) {
      totalByState.set(issue.stateName, (totalByState.get(issue.stateName) ?? 0) + 1);
    }

    for (const group of groupShownIssues(shown)) {
      const totalInGroup = totalByState.get(group.stateName) ?? 0;
      const shownInGroup = group.issues.length;
      const countLabel = shownInGroup < totalInGroup ? `${shownInGroup} of ${totalInGroup}` : String(totalInGroup);
      const emoji = STATE_TYPE_EMOJI[group.stateType] ?? "▫️";
      lines.push("");
      lines.push(`${emoji} *${escapeSlackText(group.stateName)} (${countLabel})*`);
      for (const issue of group.issues) lines.push(renderIssueLine(issue));
    }

Comment thread packages/linear/src/backlog.ts Outdated
Comment on lines +112 to +116
if (data.project == null) {
throw new Error("Linear project not found or inaccessible; check the channel-mapped project ID and the token's access.");
}
projectName ??= data.project.name;
nodes.push(...data.issues.nodes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Defensive Programming / Null Safety

If the GraphQL query succeeds but returns an unexpected structure (e.g., data is null, or data.issues is null/undefined due to partial errors), accessing data.project or data.issues.nodes will throw a TypeError and crash the request.

Adding defensive guards or optional chaining ensures that invalid/unexpected API responses are handled gracefully.

Suggested change
if (data.project == null) {
throw new Error("Linear project not found or inaccessible; check the channel-mapped project ID and the token's access.");
}
projectName ??= data.project.name;
nodes.push(...data.issues.nodes);
if (data == null || data.project == null) {
throw new Error("Linear project not found or inaccessible; check the channel-mapped project ID and the token's access.");
}
projectName ??= data.project.name;
if (!data.issues?.nodes) {
throw new Error("Linear API returned an invalid response structure (missing issues or nodes).");
}
nodes.push(...data.issues.nodes);

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/slack-linear-backlog.ts`:
- Around line 46-53: Update groupShownIssues to key by both issue.stateName and
issue.stateType when retrieving and creating groups, so same-named states with
different types remain separate and retain their correct ordering and emoji.
Apply the same composite-key logic to any related group lookup or count handling
in this function.
- Around line 26-30: Update safeSlackLinkUrl to percent-encode any literal pipe
characters in the URL href before returning it, ensuring URLs inserted into
Slack’s <url|label> format cannot disrupt link parsing while preserving the
existing protocol validation.

In `@packages/slack/src/ingress.ts`:
- Around line 57-87: Update createBoundedAsyncExecutor and the callers around
the delivery acknowledgement paths at lines 280-286 and 401-417 so accepted
tasks are durably persisted before acknowledging delivery. Ensure
dispatcher/Slack failures are retried idempotently and pending work is drained
across shutdown or restart, rather than only logged and discarded; retain the
existing concurrency and maxOutstanding limits.
- Around line 440-441: Update the configuration spread logic near
maxAsyncConcurrency and maxAsyncOutstanding to check for undefined rather than
truthiness, so explicitly provided zero values are passed through to
positiveInteger validation instead of being omitted and replaced by defaults.

In `@packages/slack/test/ingress.test.ts`:
- Around line 45-191: The asynchronous tests in
packages/slack/test/ingress.test.ts (lines 45-191) rely on elapsed-time
thresholds and fixed sleeps; replace them with deferred gates that expose
handler progress and vi.waitFor assertions on completion, while preserving the
immediate acknowledgement checks. In apps/slack-events/test/app.test.ts (lines
9-10), remove the one-tick helper and wait directly with vi.waitFor for each
expected side effect; no timer-based synchronization should remain at either
site.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 706f0420-32f8-462b-8a29-36449aeb6781

📥 Commits

Reviewing files that changed from the base of the PR and between 86073f5 and d47f412.

📒 Files selected for processing (24)
  • apps/slack-events/test/app.test.ts
  • docs/configuration.md
  • docs/platforms/slack.en.md
  • docs/platforms/slack.zh-CN.md
  • packages/cli/src/config.ts
  • packages/cli/src/doctor.ts
  • packages/cli/src/linear-backlog-config.ts
  • packages/cli/src/slack-linear-backlog.ts
  • packages/cli/src/start.ts
  • packages/cli/test/config.test.ts
  • packages/cli/test/doctor.test.ts
  • packages/cli/test/linear-backlog-config.test.ts
  • packages/cli/test/slack-linear-backlog.test.ts
  • packages/cli/test/start.test.ts
  • packages/linear/src/backlog.ts
  • packages/linear/src/index.ts
  • packages/linear/test/backlog.test.ts
  • packages/slack/src/dispatcher-events.ts
  • packages/slack/src/events.ts
  • packages/slack/src/ingress.ts
  • packages/slack/src/render.ts
  • packages/slack/test/dispatcher-events.test.ts
  • packages/slack/test/events.test.ts
  • packages/slack/test/ingress.test.ts

Comment thread packages/cli/src/slack-linear-backlog.ts Outdated
Comment thread packages/cli/src/slack-linear-backlog.ts Outdated
Comment on lines +57 to +87
function createBoundedAsyncExecutor(input: {
concurrency: number;
maxOutstanding: number;
onError(error: unknown): void;
}) {
let active = 0;
const queue: AsyncTask[] = [];

function drain(): void {
while (active < input.concurrency && queue.length > 0) {
const task = queue.shift()!;
active += 1;
void Promise.resolve()
.then(task)
.catch(input.onError)
.finally(() => {
active -= 1;
drain();
});
}
}

return {
tryEnqueue(task: AsyncTask): boolean {
if (active + queue.length >= input.maxOutstanding) return false;
queue.push(task);
drain();
return true;
}
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not treat an in-memory enqueue as durable acceptance.

Line 417 acknowledges delivery before processing succeeds, while failures are only logged. Dispatcher/Slack failures or shutdown can therefore permanently lose acknowledged commands. Persist accepted work, or provide an idempotent retry-and-drain mechanism that survives shutdown.

Also applies to: 280-286, 401-417

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slack/src/ingress.ts` around lines 57 - 87, Update
createBoundedAsyncExecutor and the callers around the delivery acknowledgement
paths at lines 280-286 and 401-417 so accepted tasks are durably persisted
before acknowledging delivery. Ensure dispatcher/Slack failures are retried
idempotently and pending work is drained across shutdown or restart, rather than
only logged and discarded; retain the existing concurrency and maxOutstanding
limits.

Comment thread packages/slack/src/ingress.ts Outdated
Comment thread packages/slack/test/ingress.test.ts Outdated
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.

1 participant