From 10553fe632d83c1f30d02947f738429657bbc1b2 Mon Sep 17 00:00:00 2001 From: Peter Schilling Date: Sat, 5 Sep 2026 07:58:21 -0700 Subject: [PATCH] Add, remove, and set flags for project teams, labels, and initiatives project update could only replace a project's teams and labels, and could not change initiatives at all: project create accepts --initiative, but after creation the only route was a hand-written mutation. Every collection field an update command can change should offer the same three operations, following the pattern issue update already has for labels: --team/--label/--initiative replace the whole set, --add-* appends, --remove-* detaches, all repeatable, and a replace flag cannot be combined with its add/remove flags. Linear's ProjectUpdateInput only takes a full teamIds/labelIds list, so add and remove read the project's current connection, following every page, compute the new set, and send it. Removing something the project does not have is an error that lists what it does have, rather than a silent no-op, and removing the last team errors up front because Linear rejects an empty teamIds. Initiatives have no input field: membership is an InitiativeToProject join row, so all three initiative flags diff the desired set against the current rows and run initiativeToProjectDelete then initiativeToProjectCreate. Deletes go first because a project may appear only once in an initiative hierarchy, so moving it to a parent or child initiative is rejected while the old link still exists; that is also why a UUID-shaped initiative is confirmed to exist before any link is touched, since the shared resolver passes UUIDs through unchecked. Those mutations are not transactional, so a failure part-way reports what was applied, including the already-committed project fields, and what is still pending, with the remaining flags given by initiative ID because names are not unique, instead of implying a rollback. The survey of issue update and initiative update found no other replace-only collection flag: IssueUpdateInput's releaseIds and subscriberIds and InitiativeUpdateInput's labelIds are not exposed by either command. Claude-Session: https://claude.ai/code/session_01A9qEGri4p2HZMQSuYsBmub --- CHANGELOG.md | 1 + README.md | 2 + docs/usage.md | 9 + .../references/organization-features.md | 9 + skills/linear-cli/references/project.md | 48 +- src/commands/project/project-update.ts | 606 +++++++++++++++++- .../__snapshots__/project-update.test.ts.snap | 91 ++- test/commands/project/project-update.test.ts | 549 ++++++++++++++++ 8 files changed, 1248 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c2142bc..72e60cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `issue query --state` and `issue mine --state` accept a workflow state name or ID as well as the six state types, looked up within the queried team scope (all teams under `--all-teams`, where a name matches every team's same-named state). An unknown name errors and lists the scope's states, and types and names can be mixed - `project update --content ` and `--content-file ` replace a project's long-form overview body, matching the flags `project create` already had. Previously the only way to change the body after creation was a hand-written `projectUpdate` mutation through `linear api` - `issue update --clear-due-date`, `--clear-estimate`, `--clear-parent`, `--clear-project`, and `--clear-milestone`, plus `project update --clear-lead`, `--clear-start-date`, and `--clear-target-date`, to remove a value the way `--unassign` and `--clear-cycle` already do. Each sends an explicit `null` to Linear and errors when combined with its set flag; `--clear-project` also rejects `--milestone`, since a milestone belongs to the project being removed. Previously these fields could only be changed, never cleared: cliffy treats `--due-date ""` as a missing value +- `project update` now changes teams, labels, and initiatives with the same three operations `issue update` has for labels: `--team`, `--label`, and `--initiative` replace the whole set, `--add-team`/`--add-label`/`--add-initiative` append, and `--remove-team`/`--remove-label`/`--remove-initiative` detach, all repeatable, with a replace flag rejected alongside its add/remove flags. Linear's project input only accepts a full `teamIds`/`labelIds` list, so add and remove read the project's current set (every page of it) and send the computed set; initiatives have no input field at all and go through the initiative-to-project link mutations, so `project update` previously could not change them after `project create --initiative`. Removing a team, label, or initiative the project does not have errors and lists what it does have, and a link change that fails part-way reports what was applied and what is still pending - `--json` (`-j`) on `team list`, `cycle list`, `cycle view`, `milestone list`, `milestone view`, and `project view`, the last read commands without machine-readable output. List commands emit the same `{ nodes, pageInfo }` connection shape as the other list commands, after the same filtering and ordering as the table; view commands emit the GraphQL object as fetched, including every issue rather than the ten-item preview, and `milestone view --all --json` includes every page. (A 2.0.0 entry claimed `cycle list --json`; that change never actually landed.) ([#276](https://github.com/schpet/linear-cli/issues/276); thanks @lakardion) ### Fixed diff --git a/README.md b/README.md index 6e62c1f0..e9c4e328 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,8 @@ linear project create --name "API v2" --team ENG --content-file overview.md linear project create --name "Mobile launch" --team APP --priority high --label Launch --member jane@example.com linear project update --content-file overview.md # replace the project's overview body linear project update --clear-lead --clear-target-date # remove values (also --clear-start-date) +linear project update --add-team OPS --remove-label Launch --add-initiative "Q4 Bets" # change teams, labels, initiatives incrementally +linear project update --team ENG --team OPS # replace the whole team set (--label and --initiative replace likewise) linear project comment list # list the project's discussion thread (UUID, slug, or name) linear project comment add --body "Kickoff Monday" # comment on a project linear project comment add --body "+1" --reply-to # reply in a thread diff --git a/docs/usage.md b/docs/usage.md index 2982021b..1356845c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -317,6 +317,15 @@ linear project update PROJECT-ID --content-file overview.md # Remove the lead, start date, or target date (each conflicts with its set flag) linear project update PROJECT-ID --clear-lead --clear-start-date --clear-target-date + +# Teams, labels, and initiatives are sets. --team, --label, and --initiative +# replace the whole set; --add-*/--remove-* change it incrementally. All are +# repeatable, and a replace flag cannot be combined with its add/remove flags. +linear project update PROJECT-ID --add-team OPS --remove-team APP +linear project update PROJECT-ID --add-label Launch --remove-label Beta +linear project update PROJECT-ID --add-initiative "Q4 Bets" # ID, slug, or name +linear project update PROJECT-ID --initiative "Q4 Bets" --initiative "Platform" # exactly these +# Removing a team, label, or initiative the project does not have is an error. ``` #### list projects diff --git a/skills/linear-cli/references/organization-features.md b/skills/linear-cli/references/organization-features.md index 318c6c3a..2d2328f2 100644 --- a/skills/linear-cli/references/organization-features.md +++ b/skills/linear-cli/references/organization-features.md @@ -56,6 +56,15 @@ linear project view linear project create --name "New Feature" --team DEV linear project create --name "Q1 Work" --team DEV --initiative "Q1 Goals" linear project create -i # Interactive mode + +# Change a project's teams, labels, or initiatives incrementally +linear project update --add-team OPS --remove-team APP +linear project update --add-label "Launch" --remove-label "Beta" +linear project update --add-initiative "Q1 Goals" # ID, slug, or name + +# Replace a whole set (cannot be combined with that set's --add-*/--remove-*) +linear project update --team DEV --team OPS +linear project update --initiative "Q1 Goals" --initiative "Platform" ``` ## Bulk Operations diff --git a/skills/linear-cli/references/project.md b/skills/linear-cli/references/project.md index 78715273..1ec28cfb 100644 --- a/skills/linear-cli/references/project.md +++ b/skills/linear-cli/references/project.md @@ -191,23 +191,37 @@ Description: Options: - -h, --help - Show this help. - --workspace - Target workspace (uses credentials) - -n, --name - Project name - -d, --description - Project description (max 255 characters, enforced by Linear's API) - -f, --description-file - Read project description from file (still subject to the 255-character API - limit) - --content - Project overview markdown - --content-file - Read project overview markdown from a file - -s, --status - Status (planned, started, paused, completed, canceled, backlog) - -l, --lead - Project lead (username, email, or @me). Use --clear-lead to remove it - --clear-lead - Remove the project's lead (cannot be combined with --lead) - --start-date - Start date (YYYY-MM-DD). Use --clear-start-date to remove it - --clear-start-date - Remove the project's start date (cannot be combined with --start-date) - --target-date - Target date (YYYY-MM-DD). Use --clear-target-date to remove it - --clear-target-date - Remove the project's target date (cannot be combined with --target-date) - -t, --team - Team key, name, or ID (can be repeated for multiple teams) - --label