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 skills/linear-cli/references/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ Options:
--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
--cycle <cycle> - Cycle name, number, or 'active'
--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 @@ -313,6 +314,7 @@ Options:
--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
--cycle <cycle> - Cycle name, number, or 'active'
-t, --title <title> - Title of the issue
```

Expand Down
14 changes: 13 additions & 1 deletion src/commands/issue/issue-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getPriorityDisplay } from "../../utils/display.ts"
import {
fetchParentIssueData,
getAllTeams,
getCycleIdByNameOrNumber,
getIssueId,
getIssueIdentifier,
getIssueLabelIdByNameForTeam,
Expand Down Expand Up @@ -499,6 +500,10 @@ export const createCommand = new Command()
"--milestone <milestone:string>",
"Name of the project milestone",
)
.option(
"--cycle <cycle:string>",
"Cycle name, number, or 'active'",
)
.option(
"--no-use-default-template",
"Do not use default template for the issue",
Expand All @@ -522,6 +527,7 @@ export const createCommand = new Command()
project,
state,
milestone,
cycle,
interactive,
title,
},
Expand Down Expand Up @@ -556,7 +562,7 @@ export const createCommand = new Command()
const noFlagsProvided = !title && !assignee && !dueDate &&
priority === undefined && estimate === undefined && !finalDescription &&
(!labels || labels.length === 0) &&
!team && !project && !state && !milestone && !start
!team && !project && !state && !milestone && !cycle && !start

if (noFlagsProvided && interactive) {
try {
Expand Down Expand Up @@ -761,6 +767,11 @@ export const createCommand = new Command()
)
}

let cycleId: string | undefined
if (cycle != null) {
cycleId = await getCycleIdByNameOrNumber(cycle, teamId)
}

// Date validation done at graphql level

// Convert parent identifier if provided and fetch parent data
Expand Down Expand Up @@ -799,6 +810,7 @@ export const createCommand = new Command()
teamId: teamId,
projectId: projectId || parentData?.projectId,
projectMilestoneId,
cycleId,
stateId,
useDefaultTemplate,
description: finalDescription,
Expand Down
12 changes: 12 additions & 0 deletions src/commands/issue/issue-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Command } from "@cliffy/command"
import { gql } from "../../__codegen__/gql.ts"
import { getGraphQLClient } from "../../utils/graphql.ts"
import {
getCycleIdByNameOrNumber,
getIssueId,
getIssueIdentifier,
getIssueLabelIdByNameForTeam,
Expand Down Expand Up @@ -72,6 +73,10 @@ export const updateCommand = new Command()
"--milestone <milestone:string>",
"Name of the project milestone",
)
.option(
"--cycle <cycle:string>",
"Cycle name, number, or 'active'",
)
.option("-t, --title <title:string>", "Title of the issue")
.action(
async (
Expand All @@ -88,6 +93,7 @@ export const updateCommand = new Command()
project,
state,
milestone,
cycle,
title,
},
issueIdArg,
Expand Down Expand Up @@ -213,6 +219,11 @@ export const updateCommand = new Command()
)
}

let cycleId: string | undefined
if (cycle != null) {
cycleId = await getCycleIdByNameOrNumber(cycle, teamId)
}

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

Expand Down Expand Up @@ -241,6 +252,7 @@ export const updateCommand = new Command()
if (projectMilestoneId !== undefined) {
input.projectMilestoneId = projectMilestoneId
}
if (cycleId !== undefined) input.cycleId = cycleId
if (stateId !== undefined) input.stateId = stateId

spinner?.stop()
Expand Down
5 changes: 5 additions & 0 deletions src/commands/issue/issue-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ export const viewCommand = new Command()
if (issueData.projectMilestone) {
metaParts.push(`**Milestone:** ${issueData.projectMilestone.name}`)
}
if (issueData.cycle) {
const cycleName = issueData.cycle.name ??
`Cycle ${issueData.cycle.number}`
metaParts.push(`**Cycle:** ${cycleName}`)
}
const metaLine = metaParts.length > 0
? "\n\n" + metaParts.join(" | ")
: ""
Expand Down
57 changes: 57 additions & 0 deletions src/utils/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export async function fetchIssueDetails(
state: { name: string; color: string }
project?: { name: string } | null
projectMilestone?: { name: string } | null
cycle?: { name?: string | null; number: number } | null
parent?: {
identifier: string
title: string
Expand Down Expand Up @@ -220,6 +221,10 @@ export async function fetchIssueDetails(
projectMilestone {
name
}
cycle {
name
number
}
parent {
identifier
title
Expand Down Expand Up @@ -289,6 +294,10 @@ export async function fetchIssueDetails(
projectMilestone {
name
}
cycle {
name
number
}
parent {
identifier
title
Expand Down Expand Up @@ -941,6 +950,54 @@ export async function getMilestoneIdByName(
return match.id
}

export async function getCycleIdByNameOrNumber(
cycleNameOrNumber: string,
teamId: string,
): Promise<string> {
const client = getGraphQLClient()
const query = gql(/* GraphQL */ `
query GetTeamCyclesForLookup($teamId: String!) {
team(id: $teamId) {
cycles {
nodes {
id
number
name
}
}
activeCycle {
id
number
name
}
}
}
`)
const data = await client.request(query, { teamId })
if (!data.team) {
throw new NotFoundError("Team", teamId)
}

if (cycleNameOrNumber.toLowerCase() === "active") {
if (!data.team.activeCycle) {
throw new NotFoundError("Active cycle", teamId)
}
return data.team.activeCycle.id
}

const cycles = data.team.cycles?.nodes || []
const match = cycles.find(
(c) =>
(c.name != null &&
c.name.toLowerCase() === cycleNameOrNumber.toLowerCase()) ||
String(c.number) === cycleNameOrNumber,
)
if (!match) {
throw new NotFoundError("Cycle", cycleNameOrNumber)
}
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 @@ -25,6 +25,7 @@ Options:
--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
--cycle <cycle> - Cycle name, number, or 'active'
--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 @@ -63,3 +64,13 @@ https://linear.app/test-team/issue/ENG-456/test-case-insensitive-labels
stderr:
""
`;

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

https://linear.app/test-team/issue/ENG-890/test-cycle-feature
"
stderr:
""
`;
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 @@ -24,6 +24,7 @@ Options:
--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
--cycle <cycle> - Cycle name, number, or 'active'
-t, --title <title> - Title of the issue

"
Expand Down Expand Up @@ -63,3 +64,14 @@ https://linear.app/test-team/issue/ENG-123/test-issue
stderr:
""
`;

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

✓ Updated issue ENG-123: Test Issue
https://linear.app/test-team/issue/ENG-123/test-issue
"
stderr:
""
`;
12 changes: 12 additions & 0 deletions test/commands/issue/__snapshots__/issue-view.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,15 @@ Set up Datadog dashboards for the new service.
stderr:
""
`;

snapshot[`Issue View Command - With Cycle 1`] = `
stdout:
"# TEST-890: Implement rate limiting

**Project:** API Gateway v2 | **Cycle:** Sprint 7

Add rate limiting to the API gateway.
"
stderr:
""
`;
77 changes: 77 additions & 0 deletions test/commands/issue/issue-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,80 @@ await snapshotTest({
}
},
})

// Test creating an issue with cycle
await snapshotTest({
name: "Issue Create Command - With Cycle",
meta: import.meta,
colors: false,
args: [
"--title",
"Test cycle feature",
"--team",
"ENG",
"--cycle",
"active",
"--no-interactive",
],
denoArgs: commonDenoArgs,
async fn() {
const { cleanup } = await setupMockLinearServer([
// Mock response for getTeamIdByKey()
{
queryName: "GetTeamIdByKey",
variables: { team: "ENG" },
response: {
data: {
teams: {
nodes: [{ id: "team-eng-id" }],
},
},
},
},
// Mock response for getCycleIdByNameOrNumber("active")
{
queryName: "GetTeamCyclesForLookup",
variables: { teamId: "team-eng-id" },
response: {
data: {
team: {
cycles: {
nodes: [
{ id: "cycle-1", number: 7, name: "Sprint 7" },
{ id: "cycle-2", number: 8, name: "Sprint 8" },
],
},
activeCycle: { id: "cycle-1", number: 7, name: "Sprint 7" },
},
},
},
},
// Mock response for the create issue mutation
{
queryName: "CreateIssue",
response: {
data: {
issueCreate: {
success: true,
issue: {
id: "issue-new-cycle",
identifier: "ENG-890",
url:
"https://linear.app/test-team/issue/ENG-890/test-cycle-feature",
team: {
key: "ENG",
},
},
},
},
},
},
], { LINEAR_TEAM_ID: "ENG" })

try {
await createCommand.parse()
} finally {
await cleanup()
}
},
})
Loading