Skip to content

fix(api): duplicate active rows for one linear workspace_slug are silently first-match-wins on remove-workspace — return 409 (N8, follow-up to #306) #883

Description

@scottschreckengaust

Parent context: surfaced during PR #681 review by @isadeks (issue #306), review 5181793802 finding N8. Deferred from #681 deliberately because it changes API semantics; a bugfix PR is the wrong vehicle. Needs maintainer approved before implementation (ADR-003).

Finding

DELETE /v1/linear/workspaces/{slug} resolves the target row by scanning LinearWorkspaceRegistryTable with a FilterExpression on workspace_slug + status = 'active', then takes Items?.[0] and stops on the first match (cdk/src/handlers/linear-remove-workspace.ts, the do { … } while (!row && scanKey) lookup).

If two active rows share a workspace_slug, one is torn down and the other keeps status='active' and a live OAuth secret — and the caller is told the removal succeeded. The operator has no signal that a second live grant survives.

This is the same absent-vs-ambiguous conflation the rest of #681 was tightened to avoid: "I found a row" is being reported as "I found the row".

How duplicates arise

workspace_slug is the Linear urlKey, which is not immutable. A workspace renamed in Linear frees its old urlKey; a different workspace can then take it and be onboarded. Nothing in bgagent linear setup / add-workspace enforces uniqueness on workspace_slug — the table's key schema is on linear_workspace_id, so two distinct workspace ids may legitimately carry the same slug.

Note this is not reachable via the removal path itself: #681 added ConditionExpression: '#status = :active' to the revoke, so concurrent DELETEs cannot both succeed on the same row. N8 is about two different rows that share a slug.

Suggested fix (reviewer's wording)

Consider paginating to completion and returning 409 on a match count > 1.

Concretely:

  • Continue the scan past the first match to the end of the keyspace (bounded by the existing MAX_SCAN_PAGES guard added in feat(cli): bgagent linear remove-workspace + DELETE route + fail-closed resolver (#306) #681) and collect all matches.
  • On matches.length > 1, return 409 with a distinct error code (e.g. WORKSPACE_SLUG_AMBIGUOUS) whose body lists the colliding linear_workspace_id values, so the operator can re-issue the removal against an unambiguous identifier.
  • Leave the single-match path byte-for-byte as-is.

Open design question worth settling in review: should the endpoint additionally accept linear_workspace_id as a disambiguator so a 409 is recoverable through the API rather than only through the manual runbook? Without it, the 409 is honest but leaves the operator in LINEAR_SETUP_GUIDE.md's manual fallback.

Why this is a semantics change, not a bugfix

Adding a 409 introduces a response the CLI and any other client must handle, and the paginate-to-completion change makes the lookup cost proportional to the whole table rather than to the position of the first match. Both belong behind their own review.

Acceptance criteria

  • Lookup collects all active matches for the slug, bounded by MAX_SCAN_PAGES.
  • matches.length > 1 → 409 + distinct error code naming the colliding workspace ids; no revoke, no DeleteSecret, no row delete.
  • Single-match and zero-match behaviour unchanged (200 / 404).
  • Handler test seeding two active rows with one workspace_slug asserts the 409 and asserts smSend was never called.
  • Error code documented wherever WORKSPACE_NOT_FOUND / SECRET_DELETE_FAILED are (docs/guides/LINEAR_SETUP_GUIDE.md), and cli/src/types.ts updated if the response shape grows.
  • CLI surfaces the 409 with the colliding ids rather than a generic failure.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    adaptersThird-party integrations: Linear, Slack, GitHub App, notification/deploy providersbugSomething isn't workinginfra-cdkCDK stacks/constructs, bootstrap, deploy topology, tags, IAM wiring, teardownv1Version 1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions