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
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | /app/admin/integrations | Click "Configure" on Linear card | โ | โ | โ | โ |
| 2 | Admin | edit form | Paste API key | โ | โ | โ | โ |
| 3 | Admin | edit form | Click "Test" | POST /api/admin/integrations/linear/test | โ | calls Linear viewer { id name email } query | โ |
| 4 | Admin | edit form | Pick team, project, default status (dropdowns populated from Linear teams/projects/states) | โ | โ | โ | โ |
| 5 | Admin | edit form | Click "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 |
| 6 | Frontend | configured-state card | "Active" with team + project name | โ | โ | โ | โ |
Path B โ Single export (one issue โ one Linear ticket)
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | issue detail or triage card | Click "Export to Linear" | โ | โ | โ | โ |
| 2 | Admin | LinearExportDialog | Confirm 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_status | Linear API: issueCreate mutation | issue.exported_to_linear |
| 3 | Frontend | dialog | Toast: "Exported as DEV-123" with link | โ | โ | โ | โ |
Path C โ Group export (N Hackorda issues โ 1 Linear ticket)
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | issues list (with multi-select) | Pick N issues, click "Export to Linear" | โ | โ | โ | โ |
| 2 | Admin | LinearExportDialog | Group mode โ pick a parent title, confirm team/project/state | POST /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 issue | issue.exported_to_linear |
| 3 | Frontend | dialog | Toast: "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_integrationsand 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_urlmatches 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_idset), 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
| # | Scenario | Expected behavior |
|---|---|---|
| N-1 | Bad API key | Test endpoint surfaces "auth_failed"; Save still works (verify) |
| N-2 | Linear rate limit during export | Friendly error; no DB write |
| N-3 | Cancel 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-4 | Group export with 0 issues | UI blocks |
| N-5 | Tester opens issue detail | "Export to Linear" button hidden (admin only) |
| N-6 | Multi-tenant: org A's admin exports an org B issue | Verify: 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_urlpopulated 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.tsxsrc/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.tssrc/app/api/admin/linear/status/route.tssrc/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.disconnectedissue.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_statusback is deferred. Today,external_statusis 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).