Skip to content

feat: support project and cycle assignment on issue create/update - #45

Closed
seraph-pixelperfect wants to merge 1 commit into
feat/issue-update-assignee-labelsfrom
feat/project-cycle-assignment
Closed

seraph-pixelperfect wants to merge 1 commit into
feat/issue-update-assignee-labelsfrom
feat/project-cycle-assignment

Conversation

@seraph-pixelperfect

Copy link
Copy Markdown
Collaborator

Closes #25. Depends on #44.

What

--project <name> and --cycle <current|number> on both issue create and issue update (parity), resolved to ids before any mutation via the v0.2 helpers:

  • --project resolves through resolveProjectId (Add projects command and issues --project filter #20) wrapped in a new loud requireProjectId (see decision 1 below); the id flows into the mutation through the existing omit-null builders as projectId.
  • --cycle current resolves the active cycle to an id via the new fetchActiveCycles — cycles(filter: { isActive: { eq: true } }), optionally scoped team: { key: { eq: $teamKey } } — the same isActive comparator Add cycle support (issues --cycle current, cycles) #21 put into IssueFilter.cycle, now on the write path.
  • --cycle <number> resolves via the new resolveCycleIdByNumber — cycles(filter: { number: { eq: $number }, team: { key: { eq: $teamKey } } }) — and mirrors Add cycle support (issues --cycle current, cycles) #21's read-path rule: a bare number fails loud (--cycle <number> requires --team (cycle numbers restart per team)), before any network request.
  • Idempotent no-ops: ISSUE_DETAIL_FIELDS now selects project { id name } cycle { id number }; update compares the issue's current ids against the resolved ones, skips matching fields (the feat: support assignee and label changes in issue update #44 assignee/label convention), and reports already up to date (no-op) when nothing would be sent.

Schema verification (source: npm pack @linear/sdk → dist/index-zxW4m1xd.d.mts, v90.0.0)

  • IssueCreateInput.cycleId?: InputMaybe<Scalars["String"]> — "The cycle associated with the issue."
  • IssueCreateInput.projectId?: InputMaybe<Scalars["String"]> — "The project associated with the issue."
  • IssueUpdateInput carries the same two optional Strings with the same doc comments — hence $projectId: String / $cycleId: String in the omit-null builders, exactly like stateId/assigneeId.
  • QueryCyclesArgs.filter?: InputMaybe<CycleFilter>; CycleFilter.isActive?: BooleanComparator (eq), CycleFilter.number?: NumberComparator (eq, Float), CycleFilter.team?: TeamFilter (whose key StringComparator resolveTeamId already relies on) — both cycle documents are schema-valid.
  • Cycle.team is non-nullable ("The team that the cycle belongs to. Each cycle is scoped to exactly one team"), so active cycles fetched workspace-wide can always be attributed to their team.

Before / after (acceptance cases, stubbed fetch — no live network)

Before (base d4312bd):

$ linear-axi issue create --title "Ship v2" --team LIN --project "Q3 launch"
AxiError [VALIDATION_ERROR]: unknown flag --project
  hint: valid flags: --title, --team, --description, --label (--help always allowed)

$ linear-axi issue update LIN-1 --cycle current
AxiError [VALIDATION_ERROR]: unknown flag --cycle
  hint: valid flags: --state, --title, --priority, --description, --assignee, --label, --remove-label (--help always allowed)

After (this PR):

$ linear-axi issue create --title "Ship v2" --team LIN --project "Q3 launch"
issue:
  identifier: LIN-9
  title: Ship v2
created: LIN-9 (https://linear.app/lini/issue/LIN-9/ship-v2)
help[2]:
  Run `linear-axi issue view LIN-9` for details
  Run `linear-axi issue update LIN-9 --state "<name>"` to set state
--- wire: query ProjectByName($name: String!) {        {"name":"Q3 launch"}
--- wire: query TeamByKey($key: String!) {             {"key":"LIN"}
--- wire: mutation CreateIssue($teamId: String!, $title: String!, $projectId: String) {
    {"teamId":"team-1","title":"Ship v2","projectId":"proj-q3"}

$ linear-axi issue update LIN-1 --cycle current
issue:
  identifier: LIN-1
  title: Ship the thing
updated: LIN-1
help[1]:
  Run `linear-axi issue view LIN-1` to confirm
--- wire: query Issue($id: String!) { issue(id: $id) {   {"id":"LIN-1"}
--- wire: query ActiveCycles {                           {}
--- wire: mutation UpdateIssue($id: String!, $cycleId: String) {
    {"id":"ir-1","cycleId":"cyc-12"}

Local gates (real output)

$ pnpm build                → tsc (clean)
$ pnpm lint                 → eslint . (clean)
$ pnpm run build:skill --check
skills/linear-axi/SKILL.md is up to date.
$ pnpm test
 Test Files  15 passed (15)
      Tests  173 passed (173)     ← 149 on the base + 24 new in test/project-cycle-assignment.test.ts
$ pnpm run format:check     → pre-existing RED: the same 18 files as the base branch (CI does not run this gate); the new test file passes `prettier --check` individually.

Tests

test/project-cycle-assignment.test.ts (24 cases, stubbed fetch, no network): create with --project / --cycle current / --cycle 3 --team LIN / both flags together; unknown project and no-active-cycle loud failures (no mutation); update --project (move, no-op when already set, set when previously unset, loud miss); update --cycle current (single active cycle success, multi-active-cycle ambiguity error listing cycle N (TEAM) candidates, foreign-team rejection, --team scoping, --team mismatch rejection, numbered resolution, requires---team before any network request, unknown cycle number, no-op when already in the cycle); parity/guards (--team alone on update, blank/invalid values, the extended nothing-to-update guard, help text). All 149 pre-existing tests are untouched and pass.

Design decisions / ambiguities flagged

  1. resolveProjectId is silent on no-match; writes are loud. The read path keeps resolveProjectId's silent-undefined semantics (Add projects command and issues --project filter #20 behavior, exercised by its tests); mutations use the new requireProjectId wrapper that throws Project "X" not found in your workspace — silently creating an issue with no project would surprise, so this follows resolveStateIdByName/resolveUserId's loud convention for mutations.
  2. --cycle current without --team on update: error-if-ambiguous (recommended option). Mirroring Add cycle support (issues --cycle current, cycles) #21, "current" may match any team's active cycle; for a write, more than one active cycle fails loud listing the candidates (Active cycles: cycle 12 (LIN), cycle 7 (ENG)) rather than first-matching — a silent wrong-team assignment is worse. As an extra guard, when the issue's team is known and the single resolved active cycle belongs to a different team, the command rejects it too. On create this cannot arise (--team is required, so the lookup is always team-scoped).
  3. No-op granularity is field-level, not flag-level. A matching project/cycle is skipped like a matching assignee/label set; only when nothing would be sent does the command report already up to date (no-op). (The pre-existing --state short-circuit, which returns early even when other flags are pending, was left untouched — out of scope here.)
  4. --team on update exists only to scope --cycle (create already requires it); passed without --cycle it fails loud, and it must match the issue's own team — moving issues between teams is not supported and a foreign team's cycle would be a wrong-team assignment.
  5. Overlapping active cycles within one team (pathological in Linear) also fail loud ("pick one by number") rather than first-matching.

Stacked PR note

Stacked on #44 (stack: #37 → #38 → #39 → #40 → #41 → #42 → #43 → #44 → this PR). No CI checks will appear on this PR: ci.yml only triggers on PRs targeting main. CI runs when retargeted to main after the stack merges; local gates below are the evidence. Do not retarget.

CI quota-block caveat: if checks do appear after retargeting and show failures, check whether they are GitHub Actions billing/spending-limit rejections rather than real failures — inspect gh run view <run-id> (or the jobs API): a billing annotation, an empty steps list, and a ~3-4s total duration indicate quota-blocked runs. If quota-blocked, treat the local gate output above as the evidence and do not retry.


Automated agent dispatch authenticated as seraph-pixelperfect, submitted for human review — not self-approved. Merge is a human decision.

@ghostinprod-pixelperfect

Copy link
Copy Markdown
Collaborator

Superseded by merged rc/0.2 PR #52 (commit ad6b141), which consolidated this write/transport stack with validation and review fixes.

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