Skip to content

feat: add bulk-create script for importing multiple issues with media#18

Open
dandaka wants to merge 1 commit into
wrsmith108:mainfrom
dandaka:feature/bulk-create
Open

feat: add bulk-create script for importing multiple issues with media#18
dandaka wants to merge 1 commit into
wrsmith108:mainfrom
dandaka:feature/bulk-create

Conversation

@dandaka

@dandaka dandaka commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds scripts/bulk-create.ts — a manifest-driven importer that creates N Linear issues in one team, each with its own markdown description and optional media files. Media is uploaded via Linear's fileUpload API and embedded in the issue description (images inline, other files as links).

Fills a gap between the existing scripts:

  • create-issue-with-project.ts creates one issue with flags
  • sync.ts / linear-ops.ts update existing issues in bulk
  • bulk-create.ts creates many new issues from a structured manifest, each with attached media

Use case

Bulk-importing customer feedback, retrospective action items, or custdev tickets where each issue needs its own screenshots or recordings — the kind of input that's too repetitive for one-off create-issue calls and too rich for a flat CSV. Used it locally to create 8 Linear issues in a single run from Telegram feedback, each with 1–2 attached screenshots/videos embedded in the description.

Usage

LINEAR_API_KEY=xxx npx tsx scripts/bulk-create.ts \
  --manifest ./feedback-2026-04 \
  --config ./feedback-2026-04/config.json

Manifest directory layout:

feedback-2026-04/
  tickets.json        # [{ key, title, priority?, labels?, files? }, ...]
  config.json         # { team_key, state_name?, default_priority? }
  desc-<key>.md       # description markdown per ticket (optional)
  <media files>       # referenced by each ticket's files[]

Config resolves by name, not UUID:

  • team_key → looked up via findTeamByKey helper (e.g. "ENG")
  • state_name → optional workflow state (e.g. "Triage")
  • Ticket labels → label names resolved per team; unknown names warn, not fail

Conventions followed

  • Uses getLinearClient() + findTeamByKey() from scripts/lib/linear-utils.ts
  • EXIT_CODES from scripts/lib/exit-codes.ts for distinct exit codes (1=missing key, 2=invalid args, 3=resource not found, 4=API error, 5=validation)
  • Label and workflow-state resolution by name (per-team rawRequest GraphQL, same pattern as create-issue-with-project.ts)
  • Added as a new esbuild entry point — auto-picked up by scripts/build.mjs
  • 6 smoke tests in scripts/__tests__/bulk-create.test.ts covering arg parsing and exit codes; wired into npm test

Verification

npm run typecheck   # passes
npm run lint        # passes
npm run build       # Built 11 entry points to dist/, Built 3 test files
npm test            # 13/13 pass (7 existing + 6 new)

Test plan

  • npm run typecheck
  • npm run lint
  • npm run build
  • npm test (13/13 pass)
  • End-to-end: created 8 issues in a real Linear team with image + video attachments

@wrsmith108 wrsmith108 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the contribution! Useful capability — bulk import with media attachments fills a real gap. Ran a multi-perspective review and surfaced a few items that I'd like addressed before merge.

Critical / High issues

1. package.json test script is a regression (drops 5 test files)

-    "test": "node --test dist/__tests__/smoke.test.js",
+    "test": "node --test dist/__tests__/smoke.test.js dist/__tests__/bulk-create.test.js",

Current main uses (or has been moving to) a dist/__tests__/*.test.js glob — please double-check. Replacing it with a hand-listed two-file set silently drops the other test files (lin-cli.test, retry.test, issue-description.test, lint-issues.test, validate-description.test).

Suggested fix: drop the package.json change entirely — the existing glob already picks up bulk-create.test.js once it's built.

2. No 429 / rate-limit handling

PR #23 (just merged) added scripts/lib/retry.ts (withRetry) specifically for this. bulk-create makes N × (label-lookup + state-lookup + fileUpload + PUT + createIssue) calls in a tight loop and ignores 429/5xx entirely. Any >5-ticket run on a busy org will fail mid-batch with no recovery.

Suggested fix: wrap each Linear API call in withRetry({ label: 'bulk-create:<op>' }). ~10 lines.

3. Whole-batch abort on per-ticket failure

for (const ticket of tickets) {
  // ...
  const asset = await uploadAsset(client, full);  // throws → kills entire run
  // ...
}

An exception in uploadAsset or createIssue bubbles to top-level main().catch(). Remaining tickets never run, summary never prints, no resume token. The PR description mentions running this on 8 tickets with video — exactly the partial-failure profile.

Suggested fix: wrap the per-ticket loop body in try/catch, collect failures[] alongside results[], print both in the summary, exit non-zero iff any failure. Goes hand-in-hand with the failure-summary issue below.

4. Code duplication with create-issue-with-project.ts

lookupStateIdByName / lookupLabelIdsByName are near-verbatim copies of lookupStateByName / lookupLabelIds in create-issue-with-project.ts. scripts/lib/linear-utils.ts is the documented home for shared lookups (per CLAUDE.md).

Suggested fix: move both helpers into linear-utils.ts as findWorkflowStateIdByName / findLabelIdsByName, have both scripts import them. Then add withRetry in one place.

5. No re-run safety / idempotency

Re-running the same manifest creates duplicates. Combined with #3, the user's flow becomes: run → one ticket fails → fix the file → re-run → 7 duplicates of the originally-successful ones.

Suggested fix: add --dry-run (trivial). Real dedup against existing issues can be a follow-up issue.

6. Failure summary lists only successes

When 3 of 8 tickets fail, Created 5/8 shows the 5 wins but no list of which failed or why. With current abort-semantics, the summary isn't even reached.

Suggested fix: along with the per-ticket isolation fix in #3, print a === FAILURES === section with key, title, and error message per failure.

Medium issues

  • Inconsistent failure handling: created.issue == null soft-fails (continue), but client.createIssue() rejection is fatal. Same logical failure, two outcomes.
  • Label case sensitivity: Linear's in filter is case-sensitive, but the missing-list is computed case-insensitively. Result: Bug vs bug silently drops the label, prints a warning, but creates the issue without it. The PR description says "warn not fail" but doesn't note this nuance.
  • Memory: readFileSync loads entire file (Linear's per-file cap is 25MB) into memory; sequential 8×25MB momentarily allocates ~200MB. createReadStream or streaming via Blob would be cleaner.
  • Hardcoded Cache-Control header: set, then overwritten by uf.headers loop — effectively a no-op default. Either remove or document.
  • Sibling script vs subcommand: linear-ops.ts is the canonical CLI per CLAUDE.md. New top-level scripts dilute discoverability. Consider linear-ops bulk-create (or bulk-import).
  • Fragile parseArgs: fixed i += 2 step assumes strict --key value pairs. No --flag=value, no -h/--help, unknown flags silently ignored. Acceptable for now but worth a CLI lib swap eventually.
  • Strict mode: missing labels and unset state_name warn but proceed → exit code says success. Consider --strict flag mirroring create-issue-with-project.ts.

Low issues

  • Smoke tests cover only arg-validation paths — no test for label/state lookup fallback, upload error handling, or partial-failure summary.
  • client.createIssue(input as never) casts away type safety. Use IssueCreateInput.
  • "bulk-create" overlaps semantically with sync.ts (bulk-update). bulk-import would disambiguate.
  • Mixed console.log / console.error / process.stdout.write; no --quiet or --json mode for scripting.

Recommendation

request-changes — useful capability, but the package.json regression, missing retry/backoff, abort-on-failure, and duplicated lookups need to land first. The fixes are scoped and self-contained. Happy to review again once those are in. Thanks for the work!

Creates N Linear issues from a manifest directory with per-ticket
markdown descriptions and optional media files. Media is uploaded
via Linear's fileUpload API and embedded in the issue description.

Use case: bulk-importing customer feedback, retrospective action
items, or custdev tickets that each need screenshots/recordings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dandaka dandaka force-pushed the feature/bulk-create branch from 3789e6c to 83b786b Compare May 6, 2026 17:07
@dandaka

dandaka commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! Rebased onto current main and addressed all requested changes. Here's a point-by-point summary:

Critical / High — all addressed

1. package.json test regression — Fixed. Dropped the hand-listed two-file set entirely. Now uses the dist/__tests__/*.test.js glob from main, which automatically picks up bulk-create.test.js alongside all existing test files. All 88 tests pass (Node 20 + 22).

2. No 429 / rate-limit handling — Fixed. All Linear API calls (fileUpload, createIssue, findTeamByKey) and the upload PUT are now wrapped in withRetry from scripts/lib/retry.ts (merged in PR #23).

3. Whole-batch abort on per-ticket failure — Fixed. Per-ticket loop body is wrapped in try/catch. Failures are collected in a failures[] array, printed in a === FAILURES === summary section with key, title, and error message. Exit is non-zero when any ticket fails.

4. Code duplication with create-issue-with-project.ts — Fixed. Extracted findWorkflowStateIdByName and findLabelIdsByName into scripts/lib/linear-utils.ts (re-exported via lib/index.ts). Both bulk-create.ts and create-issue-with-project.ts now import the shared helpers.

5. No re-run safety / --dry-run — Fixed. Added --dry-run flag that previews parsed tickets, resolved labels/states, and media files without requiring LINEAR_API_KEY or making any API calls. Real dedup against existing issues deferred to a follow-up as suggested.

6. Failure summary lists only successes — Fixed as part of #3. Summary now shows both === CREATED === and === FAILURES === sections.

Medium — addressed

  • Inconsistent failure handling: unified — API rejections are now caught per-ticket same as null-issue checks.
  • Label case sensitivity: findLabelIdsByName uses .toLowerCase() comparison. Unknown labels warn by default, fail with --strict.
  • Memory / streaming: uploads use createReadStream with duplex: 'half' for the PUT body. Only JSON config and small markdown descriptions use readFileSync.
  • Hardcoded Cache-Control: removed the no-op default.

Residual notes (not blocking)

  • Label fetch is capped at first: 250 with no pagination — sufficient for real-world teams but worth noting.
  • No package.json script alias for bulk-create (invoked directly per docs) — can add if preferred.

CI green, typecheck/lint clean, 88/88 tests pass. Ready for re-review.

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