Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ linear issue list -a # open issue list in Linear.app
linear issue start # create/switch to issue branch and mark as started
linear issue create # create a new issue (interactive prompts)
linear issue create -t "title" -d "description" # create with flags
linear issue create --project "My Project" --milestone "Phase 1" # create with milestone
linear issue update # update an issue (interactive prompts)
linear issue update ENG-123 --milestone "Phase 2" # set milestone on existing issue
linear issue delete # delete an issue
linear issue comment list # list comments on current issue
linear issue comment add # add a comment to current issue
Expand Down
2 changes: 2 additions & 0 deletions skills/linear-cli/references/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ Options:
--team <team> - Team associated with the issue (if not your default team)
--project <project> - Name of the project with the issue
-s, --state <state> - Workflow state for the issue (by name or type)
--milestone <milestone> - Name of the project milestone
--no-use-default-template - Do not use default template for the issue
--no-interactive - Disable interactive prompts
-t, --title <title> - Title of the issue
Expand Down Expand Up @@ -311,6 +312,7 @@ Options:
--team <team> - Team associated with the issue (if not your default team)
--project <project> - Name of the project with the issue
-s, --state <state> - Workflow state for the issue (by name or type)
--milestone <milestone> - Name of the project milestone
-t, --title <title> - Title of the issue
```

Expand Down
26 changes: 25 additions & 1 deletion src/commands/issue/issue-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getIssueLabelIdByNameForTeam,
getIssueLabelOptionsByNameForTeam,
getLabelsForTeam,
getMilestoneIdByName,
getProjectIdByName,
getProjectOptionsByName,
getTeamIdByKey,
Expand Down Expand Up @@ -494,6 +495,10 @@ export const createCommand = new Command()
"-s, --state <state:string>",
"Workflow state for the issue (by name or type)",
)
.option(
"--milestone <milestone:string>",
"Name of the project milestone",
)
.option(
"--no-use-default-template",
"Do not use default template for the issue",
Expand All @@ -516,6 +521,7 @@ export const createCommand = new Command()
team,
project,
state,
milestone,
interactive,
title,
},
Expand Down Expand Up @@ -550,7 +556,7 @@ export const createCommand = new Command()
const noFlagsProvided = !title && !assignee && !dueDate &&
priority === undefined && estimate === undefined && !finalDescription &&
(!labels || labels.length === 0) &&
!team && !project && !state && !start
!team && !project && !state && !milestone && !start

if (noFlagsProvided && interactive) {
try {
Expand Down Expand Up @@ -738,6 +744,23 @@ export const createCommand = new Command()
}
}

let projectMilestoneId: string | undefined
if (milestone != null) {
if (projectId == null) {
throw new ValidationError(
"--milestone requires --project to be set",
{
suggestion:
"Use --project to specify which project the milestone belongs to.",
},
)
}
projectMilestoneId = await getMilestoneIdByName(
milestone,
projectId,
)
}

// Date validation done at graphql level

// Convert parent identifier if provided and fetch parent data
Expand Down Expand Up @@ -775,6 +798,7 @@ export const createCommand = new Command()
labelIds,
teamId: teamId,
projectId: projectId || parentData?.projectId,
projectMilestoneId,
stateId,
useDefaultTemplate,
description: finalDescription,
Expand Down
29 changes: 29 additions & 0 deletions src/commands/issue/issue-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
getIssueId,
getIssueIdentifier,
getIssueLabelIdByNameForTeam,
getIssueProjectId,
getMilestoneIdByName,
getProjectIdByName,
getTeamIdByKey,
getWorkflowStateByNameOrType,
Expand Down Expand Up @@ -66,6 +68,10 @@ export const updateCommand = new Command()
"-s, --state <state:string>",
"Workflow state for the issue (by name or type)",
)
.option(
"--milestone <milestone:string>",
"Name of the project milestone",
)
.option("-t, --title <title:string>", "Title of the issue")
.action(
async (
Expand All @@ -81,6 +87,7 @@ export const updateCommand = new Command()
team,
project,
state,
milestone,
title,
},
issueIdArg,
Expand Down Expand Up @@ -187,6 +194,25 @@ export const updateCommand = new Command()
}
}

let projectMilestoneId: string | undefined
if (milestone != null) {
const milestoneProjectId = projectId ??
await getIssueProjectId(issueId)
if (milestoneProjectId == null) {
throw new ValidationError(
"--milestone requires --project to be set (issue has no existing project)",
{
suggestion:
"Use --project to specify the project for the milestone.",
},
)
}
projectMilestoneId = await getMilestoneIdByName(
milestone,
milestoneProjectId,
)
}

// Build the update input object, only including fields that were provided
const input: Record<string, string | number | string[] | undefined> = {}

Expand All @@ -212,6 +238,9 @@ export const updateCommand = new Command()
if (labelIds.length > 0) input.labelIds = labelIds
if (teamId !== undefined) input.teamId = teamId
if (projectId !== undefined) input.projectId = projectId
if (projectMilestoneId !== undefined) {
input.projectMilestoneId = projectMilestoneId
}
if (stateId !== undefined) input.stateId = stateId

spinner?.stop()
Expand Down
15 changes: 14 additions & 1 deletion src/commands/issue/issue-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,20 @@ export const viewCommand = new Command()
}

const { identifier } = issueData
let markdown = `# ${identifier}: ${title}${

// Build metadata line with project and milestone
const metaParts: string[] = []
if (issueData.project) {
metaParts.push(`**Project:** ${issueData.project.name}`)
}
if (issueData.projectMilestone) {
metaParts.push(`**Milestone:** ${issueData.projectMilestone.name}`)
}
const metaLine = metaParts.length > 0
? "\n\n" + metaParts.join(" | ")
: ""

let markdown = `# ${identifier}: ${title}${metaLine}${
description ? "\n\n" + description : ""
}`

Expand Down
62 changes: 62 additions & 0 deletions src/utils/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ export async function fetchIssueDetails(
url: string
branchName: string
state: { name: string; color: string }
project?: { name: string } | null
projectMilestone?: { name: string } | null
parent?: {
identifier: string
title: string
Expand Down Expand Up @@ -212,6 +214,12 @@ export async function fetchIssueDetails(
name
color
}
project {
name
}
projectMilestone {
name
}
parent {
identifier
title
Expand Down Expand Up @@ -275,6 +283,12 @@ export async function fetchIssueDetails(
name
color
}
project {
name
}
projectMilestone {
name
}
parent {
identifier
title
Expand Down Expand Up @@ -879,6 +893,54 @@ export async function getTeamMembers(teamKey: string) {
)
}

export async function getIssueProjectId(
issueIdentifier: string,
): Promise<string | undefined> {
const client = getGraphQLClient()
const query = gql(/* GraphQL */ `
query GetIssueProjectId($id: String!) {
issue(id: $id) {
project {
id
}
}
}
`)
const data = await client.request(query, { id: issueIdentifier })
return data.issue?.project?.id ?? undefined
}

export async function getMilestoneIdByName(
milestoneName: string,
projectId: string,
): Promise<string> {
const client = getGraphQLClient()
const query = gql(/* GraphQL */ `
query GetProjectMilestonesForLookup($projectId: String!) {
project(id: $projectId) {
projectMilestones {
nodes {
id
name
}
}
}
}
`)
const data = await client.request(query, { projectId })
if (!data.project) {
throw new NotFoundError("Project", projectId)
}
const milestones = data.project.projectMilestones?.nodes || []
const match = milestones.find(
(m) => m.name.toLowerCase() === milestoneName.toLowerCase(),
)
if (!match) {
throw new NotFoundError("Milestone", milestoneName)
}
return match.id
}

export async function selectOption(
dataName: string,
originalValue: string,
Expand Down
11 changes: 11 additions & 0 deletions test/commands/issue/__snapshots__/issue-create.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Options:
--team <team> - Team associated with the issue (if not your default team)
--project <project> - Name of the project with the issue
-s, --state <state> - Workflow state for the issue (by name or type)
--milestone <milestone> - Name of the project milestone
--no-use-default-template - Do not use default template for the issue
--no-interactive - Disable interactive prompts
-t, --title <title> - Title of the issue
Expand All @@ -43,6 +44,16 @@ stderr:
""
`;

snapshot[`Issue Create Command - With Milestone 1`] = `
stdout:
"Creating issue in ENG

https://linear.app/test-team/issue/ENG-789/test-milestone-feature
"
stderr:
""
`;

snapshot[`Issue Create Command - Case Insensitive Label Matching 1`] = `
stdout:
"Creating issue in ENG
Expand Down
12 changes: 12 additions & 0 deletions test/commands/issue/__snapshots__/issue-update.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Options:
--team <team> - Team associated with the issue (if not your default team)
--project <project> - Name of the project with the issue
-s, --state <state> - Workflow state for the issue (by name or type)
--milestone <milestone> - Name of the project milestone
-t, --title <title> - Title of the issue

"
Expand All @@ -41,6 +42,17 @@ stderr:
""
`;

snapshot[`Issue Update Command - With Milestone 1`] = `
stdout:
"Updating issue ENG-123

✓ Updated issue ENG-123: Test Issue
https://linear.app/test-team/issue/ENG-123/test-issue
"
stderr:
""
`;

snapshot[`Issue Update Command - Case Insensitive Label Matching 1`] = `
stdout:
"Updating issue ENG-123
Expand Down
14 changes: 14 additions & 0 deletions test/commands/issue/__snapshots__/issue-view.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ stdout:
"name": "In Progress",
"color": "#f87462"
},
"project": null,
"projectMilestone": null,
"parent": null,
"children": [],
"comments": [
Expand Down Expand Up @@ -183,3 +185,15 @@ Add user authentication to the application.
stderr:
""
`;

snapshot[`Issue View Command - With Project And Milestone 1`] = `
stdout:
"# TEST-789: Add monitoring dashboards

**Project:** Platform Infrastructure Q1 | **Milestone:** Phase 2: Observability

Set up Datadog dashboards for the new service.
"
stderr:
""
`;
Loading