Hackorda Docs
Flows

F 15 Linear Export

Status: ๐ŸŸข ready to test Owner: Adilet Last updated: 2026-05-08 Last verified on production: โ€”

Goal

An admin configures Linear (API key + team + project + status mapping) through the admin UI, then exports issues from Hackorda to Linear in single mode (one issue โ†’ one Linear ticket) or group mode (multi-select several Hackorda issues โ†’ one Linear ticket with a checklist body). The external_id / external_url is stored on the issue for round-trip linking.

Actors

  • Primary โ€” Admin (configures + exports)
  • Secondary โ€” Linear API; engineering team (reads the exported tickets)

Preconditions

  • Admin is signed in.
  • A Linear workspace exists with at least one team and one project.
  • A Linear API key with create-issue scope.

Trigger

For configuration: admin opens /app/admin/integrations โ†’ Linear card. For export: admin opens any Hackorda issue or the triage queue.

Path A โ€” Configure Linear

#ActorUI surfaceActionAPI callDB writesSide effectsSuggested event
1Admin/app/admin/integrationsClick "Configure" on Linear cardโ€”โ€”โ€”โ€”
2Adminedit formPaste API keyโ€”โ€”โ€”โ€”
3Adminedit formClick "Test"POST /api/admin/integrations/linear/testโ€”calls Linear viewer { id name email } queryโ€”
4Adminedit formPick team, project, default status (dropdowns populated from Linear teams/projects/states)โ€”โ€”โ€”โ€”
5Adminedit formClick "Save"PUT /api/admin/integrations/linear body { apiKey, teamId, projectId, defaultStateId }upsert organization_integrations row with provider_slug='linear', config={ team_id, project_id, default_state_id, api_key }, status='active'โ€”integration.configured
6Frontendconfigured-state card"Active" with team + project nameโ€”โ€”โ€”โ€”

Path B โ€” Single export (one issue โ†’ one Linear ticket)

#ActorUI surfaceActionAPI callDB writesSide effectsSuggested event
1Adminissue detail or triage cardClick "Export to Linear"โ€”โ€”โ€”โ€”
2AdminLinearExportDialogConfirm team / project / state (default from config)POST /api/admin/linear/export body { mode: 'single', issueId }UPDATE issues SET external_id, external_url, external_synced_at, external_statusLinear API: issueCreate mutationissue.exported_to_linear
3FrontenddialogToast: "Exported as DEV-123" with linkโ€”โ€”โ€”โ€”

Path C โ€” Group export (N Hackorda issues โ†’ 1 Linear ticket)

#ActorUI surfaceActionAPI callDB writesSide effectsSuggested event
1Adminissues list (with multi-select)Pick N issues, click "Export to Linear"โ€”โ€”โ€”โ€”
2AdminLinearExportDialogGroup mode โ€” pick a parent title, confirm team/project/statePOST /api/admin/linear/export body { mode: 'group', issueIds, groupTitle }UPDATE issues SET external_id, external_url, external_synced_at, external_status (same parent ticket on N rows)Linear: one issueCreate mutation, body has checklist of links to each Hackorda issueissue.exported_to_linear
3FrontenddialogToast: "Exported N issues as DEV-124"โ€”โ€”โ€”โ€”

Acceptance criteria

  • AC-1 โ€” Given a valid API key + team + project + state, when admin saves, then the row exists in organization_integrations and an "Active" state card renders.
  • AC-2 โ€” Given the integration is active, when admin opens LinearExportDialog, then team / project / state are pre-filled from the saved config.
  • AC-3 โ€” Given a single export, when the API returns success, then issues.external_url matches the URL Linear returned.
  • AC-4 โ€” Given a group export of 3 issues, when the API returns success, then exactly 1 Linear ticket is created (verify via Linear UI), and 3 Hackorda issues share the same external_id.
  • AC-5 โ€” Given the integration is missing or inactive, when admin clicks "Export to Linear", then the dialog explains "Linear is not configured" and does not attempt the call.
  • AC-6 โ€” Given a Linear API error (auth fail / rate limit), when the export tries, then the dialog surfaces a friendly error and does NOT mutate issues.external_* columns.
  • AC-7 โ€” Given an issue is already exported (external_id set), when admin clicks "Export to Linear" again, then the dialog warns "Already exported as DEV-123" with options "Open in Linear" or "Re-export" (verify policy on prod).
  • AC-8 โ€” Given a non-admin caller, when they hit POST /api/admin/linear/export, then 403.

Test data / fixtures

  • A scratch Linear workspace + team for QA โ€” never export to a production Linear.
  • A Hackorda cycle with at least 3 approved issues for group testing.

Negative paths

#ScenarioExpected behavior
N-1Bad API keyTest endpoint surfaces "auth_failed"; Save still works (verify)
N-2Linear rate limit during exportFriendly error; no DB write
N-3Cancel mid-export (admin closes dialog)Linear ticket may be created (already happened); DB columns NOT updated โ†’ state is inconsistent โ†’ re-export logic must handle
N-4Group export with 0 issuesUI blocks
N-5Tester opens issue detail"Export to Linear" button hidden (admin only)
N-6Multi-tenant: org A's admin exports an org B issueVerify: scope check enforces same org? Currently single-tenant so this is moot.

Manual QA checklist

  • As admin: /app/admin/integrations โ†’ Linear card "Not configured"
  • Click Configure โ†’ paste API key โ†’ Test โ†’ green
  • Pick team, project, default state โ†’ Save โ†’ "Active"
  • Open any approved issue โ†’ "Export to Linear" button visible
  • Click โ†’ dialog pre-fills team/project/state
  • Confirm โ†’ toast with DEV-XXX, click "Open" โ†’ new tab to Linear ticket
  • Verify Linear ticket title matches Hackorda issue title; description has Hackorda link
  • Verify issues.external_id, external_url populated in DB
  • On the issues list, multi-select 3 issues โ†’ "Export as group"
  • Confirm โ†’ toast with DEV-YYY for the parent
  • Open the Linear ticket โ†’ body has checklist of 3 sub-items linking back to Hackorda issues
  • Verify all 3 Hackorda issues share external_id = DEV-YYY
  • Try to export a non-admin โ†’ button hidden
  • Disconnect Linear โ†’ "Export to Linear" disabled / hidden
  • Re-configure with bad key โ†’ next export fails gracefully

Automated test outline

test.describe("F-15 linear export", () => {
  test("configure โ†’ test โ†’ save", async ({ page }) => { /* mock Linear */ });
  test("single export updates external_*", async ({ request }) => { โ€ฆ });
  test("group export shares external_id", async ({ request }) => { โ€ฆ });
  test("non-admin 403", async ({ request }) => { โ€ฆ });
  test("graceful auth_failed", async ({ request }) => { /* mock 401 */ });
});

Code references

  • UI:
    • src/components/admin/integrations/LinearIntegrationCard.tsx
    • src/components/test-cycles/admin/linear/LinearExportDialog.tsx
  • Pages: src/app/app/admin/integrations/page.tsx
  • API:
    • src/app/api/admin/integrations/linear/route.ts (GET/PUT/DELETE config)
    • src/app/api/admin/integrations/linear/test/route.ts
    • src/app/api/admin/linear/status/route.ts
    • src/app/api/admin/linear/export/route.ts
  • Hooks: src/hooks/test-cycles/linear.ts, src/hooks/test-cycles/integrations.ts
  • Lib: src/lib/integrations/linear.ts (thin GraphQL fetch wrapper)
  • Schema: organization_integrations (provider_slug='linear'), issues.external_id, issues.external_url, issues.external_synced_at, issues.external_status

Events emitted (proposed)

  • integration.configured โ€” { provider: 'linear', team_id, project_id }
  • integration.tested โ€” { provider, success, error_code }
  • integration.disconnected
  • issue.exported_to_linear โ€” { issue_ids: [...], mode: 'single' \| 'group', external_id, exported_by }

Open questions / known gaps

  • Multi-tenant Linear: v1 is single-tenant. Per-org Linear config is deferred (feature-matrix.md ยง3.9).
  • Linear โ†’ Hackorda webhook to pull external_status back is deferred. Today, external_status is set once at export time and never updated.
  • Re-export semantics (overwrite vs. update) are not nailed down.
  • No "unlink from Linear" action (just clear external_* in DB).

On this page