Skip to content

feat(issue): add lc issue move subcommand - #187

Merged
bougyman merged 5 commits into
mainfrom
EXT-9-issue-move
Aug 24, 2026
Merged

feat(issue): add lc issue move subcommand#187
bougyman merged 5 commits into
mainfrom
EXT-9-issue-move

Conversation

@bougyman

Copy link
Copy Markdown
Member

Summary

  • Adds lc issue move ISSUE_ID... --project PROJECT to move one or more issues to a Linear project
  • --dry-run previews the planned moves without executing any mutations
  • --yes/-y skips the confirmation prompt (default: prompt with yes as the default answer)
  • Concurrent execution via Task.async_stream (capped at 20, same as issue status)
  • Aliases: m, mv (e.g. lc issue m --project Manhattan CRY-1 CRY-2)
  • --team/-t scopes project resolution to the given team; falls back to the first fetched issue's team

Design decisions

  • No new domain layer changes: reuses Linear.attach_issue_to_project/2 which calls the existing issueUpdate(projectId:) mutation
  • Plan always shown before any confirmation or execution (per the accessibility-first principle: every operation must be legible top-to-bottom through a screen reader)
  • Dry-run is a flag, not a mode: --dry-run simply prints the plan and returns :ok without calling any mutations
  • Team derivation: when --team is omitted and no profile is active, derives team from hd(issues).team.id to avoid requiring a separate team prompt

Test plan

  • --project moves a single issue with --yes (no prompt)
  • --dry-run shows plan but makes no API mutation call
  • User declines confirmation, no mutation called
  • User confirms, mutation is called
  • Multiple issues moved concurrently
  • --output json emits issue JSON, suppresses confirmation messages
  • No issue IDs provided → exits 22 (smells bad)
  • Alias m routes to issue move
  • Alias mv routes to issue move
  • --team scopes project resolution to the given team

Closes https://linear.app/the-rubyists/issue/EXT-9

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 24, 2026 09:55

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread app/lib/linear_cli/cli/commands.ex Outdated
@bougyman

Copy link
Copy Markdown
Member Author

Rework — code review feedback addressed

Changes pushed in commit 6783eb2:

Review issue fixed: JSON output corruption

  • print_move_plan/3 now guards on output != "json" — plan lines are suppressed when running in JSON mode
  • The JSON output test was updated to assert clean parseable JSON directly (no String.split workaround)
  • Added refute output =~ "->" assertion to confirm plan lines are absent in JSON output

Review issue addressed: idiomatic Elixir

  • Replaced nested if/else in issue_move/1 body with execute_moves_if_confirmed/4 function heads — dry_run: true, yes: true, and the prompt fallback are three separate clauses
  • Replaced value-as-error-signal pattern in resolve_move_project with a with chain and resolve_move_team_id/2 function heads (nil → use issue's team, string → look up team and extract id)

Minor addressed: Readme documentation

  • Added issue move | m, mv to the alias table
  • Added ==== Move issues to a project usage section with all five flag examples

Adds `lc issue move ISSUE_ID... --project PROJECT` to move one or more
issues to a target Linear project.

- Resolves the target project by name/URL/ID/search term via the same
  fuzzy-match + prompt logic used by issue create/update and project
  favorite (`Projects.project_for/2`)
- Accepts variadic issue IDs via `allow_unknown_args: true` (same pattern
  as issue take/status/update)
- Prints a plan line (`ISSUE_ID -> PROJECT_NAME`) for every issue before
  any mutation is attempted
- `--dry-run` prints the plan and exits without executing any mutations
- `--yes`/`-y` skips the confirmation prompt; without it, asks "Proceed
  with move?" (default: yes, matching `Prompt.yes?/1` convention)
- `--team`/`-t` scopes project resolution to the given team; when omitted,
  derives team from the first fetched issue (avoids a separate prompt)
- Moves execute concurrently via `Task.async_stream` (capped at 20,
  matching issue status pattern)
- `--output json` emits the updated issue(s) as JSON, suppressing the
  confirmation "moved to" messages
- Aliases: `m` and `mv` (e.g. `lc issue m --project PROJ ISSUE_ID`)
Comment thread app/lib/linear_cli/cli/commands.ex Outdated
…d docs

- Replace nested if/else in issue_move/1 with execute_moves_if_confirmed/4
  function heads (dry_run, yes, prompt branches as separate clauses)
- Replace value-as-error pattern in resolve_move_project with a with chain
  and resolve_move_team_id/2 function heads
- Suppress plan lines in JSON output mode (print_move_plan guards on output)
- Update JSON test to assert clean JSON without plan-line prefix workaround
- Add issue move alias row (m, mv) to Readme alias table
- Add issue move usage section to Readme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bougyman

Copy link
Copy Markdown
Member Author

Rework (Run 3)

Addressed review feedback from Run 2:

Resolved: Readme.adoc merge conflict

  • The rebase of the refactor commit was stalled mid-way with unresolved conflict markers in Readme.adoc at two locations (alias table row for issue move, and the ==== Move issues to a project documentation section).
  • Both conflicts were trivial (HEAD side was empty; incoming side had the new content). Kept the incoming content at both locations.
  • Staged the resolution and ran git rebase --continue to complete the rebase.
  • Force-pushed the branch to origin.

The staged refactoring from Run 2 is included in the pushed commit:

  • execute_moves_if_confirmed/4 multi-clause pattern matching replaces nested if/else
  • resolve_move_team_id/2 multi-clause heads replace the mixed-type variable pattern
  • print_move_plan/3 guard clause suppresses plan lines for JSON output
  • JSON test tightened to assert pure JSON without plan lines

Quality suite after rework:

  • 333 tests pass, 0 failures
  • Clean compile (warnings-as-errors)
  • No format errors
  • No credo issues

Replace case-on-nil with project_result/2 multi-clause heads in
resolve_move_project/2, and case-on-boolean with if in the
execute_moves_if_confirmed/4 fallback clause.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bougyman

Copy link
Copy Markdown
Member Author

Rework (Run 4)

Addressed: "There's still two nested cases to remove."

What was changed (app/lib/linear_cli/cli/commands.ex):

  1. (was line 549): replaced case Projects.project_for(...) with a call to new project_result/2 helper that uses multi-clause function heads — one clause for nil (returns error tuple), one for any project struct (returns ok tuple).

  2. execute_moves_if_confirmed/4 fallback clause (was line 568): replaced case Prompt.yes?(...) do true -> ... false -> ... with if Prompt.yes?(...), do: ..., else: .... Using if for a boolean-returning function is idiomatic Elixir.

Quality suite:

  • 333 tests pass, 0 failures
  • Clean compile (warnings-as-errors)
  • No format errors
  • No credo issues

Pushed as a new commit (fe26143) on top of the existing branch, no force-push.

@bougyman

Copy link
Copy Markdown
Member Author

Implementation Complete (Run 1 / All Reworks)

All quality gates pass:

  • 333 tests, 0 failures
  • Clean compile (warnings-as-errors)
  • No format errors
  • No credo issues
  • CI checks pass

Summary of what was delivered

New subcommand: lc issue move ISSUE_ID... --project PROJECT

  • Resolves the target project by name/search, scoped to the issue's team (or --team) using the existing Projects.project_for/2 fuzzy-match+prompt pipeline
  • Accepts one or more issue IDs (variadic, matching issue status/issue take)
  • Prints a move plan before doing anything (CRY-1 -> Manhattan per issue)
  • --dry-run: shows plan, exits — no API mutations
  • --yes / -y: skips confirmation prompt
  • Aliases: m, mv (e.g. lc issue m --project Manhattan CRY-1)
  • Concurrent execution capped at 20 (same as issue status)
  • JSON output suppresses plan lines, outputs pure parseable JSON

Key decisions

  • No domain-layer changes needed — reuses existing Linear.attach_issue_to_project/2
  • Team derived from first issue's .team.id when --team omitted (avoids an extra prompt)
  • Plan lines always print before confirmation so output reads correctly top-to-bottom through a screen reader

Files changed

  • app/lib/linear_cli/cli.ex — subcommand spec, dispatch, m/mv aliases
  • app/lib/linear_cli/cli/commands.exissue_move/1 and private helpers
  • Readme.adoc — alias table entry and ==== Move issues to a project docs section
  • app/test/linear_cli/cli/issue_commands_test.exs — 10 tests covering full acceptance matrix

…es_result

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bougyman

Copy link
Copy Markdown
Member Author

Rework (Run 5)

Addressed: inline review comments requesting removal of `|> case do` at line 596.

What was changed (app/lib/linear_cli/cli/commands.ex):

  • Replaced the `|> case do` in `apply_moves/3` with a pipeline into a new `display_moves_result/3` helper using multi-clause function heads:
    • `display_moves_result({:ok, updated_issues}, project, output)` — reverses the list, calls `Display.show/2`, and delegates ok-message printing to `print_move_results/3`
    • `display_moves_result(error, _project, _output)` — passes error through unchanged
  • Extracted the `if output != "json"` guard into a separate `print_move_results/3` with guard-based clause dispatch (mirrors the existing `print_move_plan/3` pattern already in the same file)

Quality suite:

  • 333 tests pass, 0 failures
  • Clean compile (warnings-as-errors)
  • No format errors
  • No credo issues

@bougyman

Copy link
Copy Markdown
Member Author

Code Review (Run 5)

Quality Suite

  • Compile: Clean (warnings-as-errors)
  • Tests: 333 passed, 0 failures
  • Format: Clean
  • Credo: No issues (strict mode)

Prior Review Issues — All Resolved

  1. Nested case/if statements (flagged Run 1, reworked Run 2-4): Fully replaced with multi-clause pattern matching (execute_moves_if_confirmed/4, resolve_move_team_id/2, project_result/2, display_moves_result/2). Clean and idiomatic.
  2. Readme.adoc merge conflict (flagged Run 2, fixed Run 3): Resolved.

Current Assessment

No critical or major issues found. The implementation is clean after four rework rounds.

What looks good

  • Multi-clause dispatch in execute_moves_if_confirmed/4 mirrors the established issue_status patterns — dry-run, yes, and interactive confirmation each get their own function head (commands.ex:564-572).
  • project_result/2 (commands.ex:553-556) properly handles nil from Projects.project_for/2, returning a structured error. This is an improvement over attach_project/2 which crashes on nil (a documented pre-existing issue).
  • Guard clause when output != "json" on print_move_plan/3 and print_move_results/3 keeps plan/confirmation lines out of JSON output cleanly.
  • Concurrent execution via Task.async_stream with the shared @max_concurrent_issue_updates cap and ordered: true — consistent with issue_status.
  • resolve_move_team_id/2 (commands.ex:558-561) — clean fallback: derive team from first issue when --team omitted, avoiding unnecessary prompts.
  • Test coverage is thorough: 10 tests covering single move, multi-move, dry-run, decline, confirm, JSON output, no-IDs error, both aliases (m/mv), and --team scoping.
  • IssueHelpers.move_issue/2 extraction with attach_project/2 delegating to it eliminates code duplication in the helpers layer.

Minor observations (not blocking)

  1. Wording change: attach_project/2 path now says "moved to" instead of "attached to" (via delegation to move_issue/2). Tests updated. Intentional and consistent, but a user-visible change to issue update --project output.
  2. apply_move/2 vs move_issue/2: issue_move calls Linear.attach_issue_to_project directly in apply_move/2 rather than going through IssueHelpers.move_issue/2. This is correct — the batch flow handles display separately, so using move_issue/2 would double-print confirmations. Just noting the two code paths exist for clarity.

Overall Assessment

Approve. The code is idiomatic, well-tested, and follows established patterns. No issues remain from prior review rounds.

@bougyman
bougyman merged commit 51cd1f7 into main Aug 24, 2026
2 checks passed
@bougyman
bougyman deleted the EXT-9-issue-move branch August 24, 2026 16:06
This was referenced Aug 24, 2026
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