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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ linear issue create --project "My Project" --milestone "Phase 1" # create with
linear issue update # update an issue (interactive prompts)
linear issue update ENG-123 --milestone "Phase 2" # set milestone on existing issue
linear issue update ENG-123 --clear-due-date --clear-parent # remove values (also --clear-estimate, --clear-project, --clear-milestone, --clear-cycle, --unassign)
linear issue archive ENG-123 --confirm # archive an 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
6 changes: 6 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ delete an issue:
linear issue delete TEAM-123
```

archive an issue:

```bash
linear issue archive TEAM-123 --confirm
```

#### issue comments

```bash
Expand Down
1 change: 1 addition & 0 deletions skills/linear-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ linear issue
linear issue agent-session
linear issue agent-session list
linear issue agent-session view
linear issue archive
linear issue attach
linear issue comment
linear issue comment add
Expand Down
19 changes: 19 additions & 0 deletions skills/linear-cli/references/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Commands:
describe [issueId] - Print the issue title and Linear-issue trailer
commits [issueId] - Show all commits for a Linear issue (jj only)
pull-request, pr [issueId] - Create a GitHub pull request with issue details
archive [issueId] - Archive an issue
delete, d [issueId] - Delete an issue
create - Create a linear issue
update [issueId] - Update a linear issue
Expand Down Expand Up @@ -98,6 +99,24 @@ Options:
-j, --json - Output as JSON
```

### archive

> Archive an issue

```
Usage: linear issue archive [issueId]

Description:

Archive an issue

Options:

-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
-y, --confirm - Skip confirmation prompt
```

### attach

> Create a sidebar link attachment on an issue (images do not render inline)
Expand Down
88 changes: 88 additions & 0 deletions src/commands/issue/issue-archive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Command } from "@cliffy/command"
import { Confirm } from "@cliffy/prompt"
import type { GraphQLClient } from "graphql-request"
import { gql } from "../../__codegen__/gql.ts"
import { getGraphQLClient } from "../../utils/graphql.ts"
import { getIssueIdentifier } from "../../utils/linear.ts"
import {
CliError,
handleError,
NotFoundError,
ValidationError,
} from "../../utils/errors.ts"

export const archiveCommand = new Command()
.name("archive")
.description("Archive an issue")
.arguments("[issueId:string]")
.option("-y, --confirm", "Skip confirmation prompt")
.action(async ({ confirm }, issueId) => {
try {
const client = getGraphQLClient()
await archiveIssue(client, issueId, { confirm })
} catch (error) {
handleError(error, "Failed to archive issue")
}
})

async function archiveIssue(
client: GraphQLClient,
issueId: string | undefined,
options: { confirm?: boolean },
): Promise<void> {
const resolvedId = await getIssueIdentifier(issueId)
if (!resolvedId) {
throw new ValidationError(
"Could not determine issue ID",
{ suggestion: "Please provide an issue ID like 'ENG-123'." },
)
}

const detailsQuery = gql(`
query GetIssueArchiveDetails($id: String!) {
issue(id: $id) {
identifier
title
}
}
`)

const issueDetails = await client.request(detailsQuery, { id: resolvedId })
if (!issueDetails.issue) {
throw new NotFoundError("Issue", resolvedId)
}

const { identifier, title } = issueDetails.issue
if (!options.confirm) {
if (!Deno.stdin.isTerminal()) {
throw new ValidationError(
"Interactive confirmation required",
{ suggestion: "Use --confirm to skip." },
)
}

const confirmed = await Confirm.prompt({
message: `Are you sure you want to archive "${identifier}: ${title}"?`,
default: false,
})
if (!confirmed) {
console.log("Archive cancelled.")
return
}
}

const archiveMutation = gql(`
mutation ArchiveIssue($id: String!) {
issueArchive(id: $id) {
success
}
}
`)

const result = await client.request(archiveMutation, { id: resolvedId })
if (!result.issueArchive.success) {
throw new CliError("Failed to archive issue")
}

console.log(`✓ Successfully archived issue: ${identifier}: ${title}`)
}
2 changes: 2 additions & 0 deletions src/commands/issue/issue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Command } from "@cliffy/command"
import { attachCommand } from "./issue-attach.ts"
import { archiveCommand } from "./issue-archive.ts"
import { commentCommand } from "./issue-comment.ts"
import { createCommand } from "./issue-create.ts"
import { deleteCommand } from "./issue-delete.ts"
Expand Down Expand Up @@ -36,6 +37,7 @@ export const issueCommand = new Command()
.command("describe", describeCommand)
.command("commits", commitsCommand)
.command("pull-request", pullRequestCommand)
.command("archive", archiveCommand)
.command("delete", deleteCommand)
.command("create", createCommand)
.command("update", updateCommand)
Expand Down
45 changes: 45 additions & 0 deletions test/commands/issue/__snapshots__/issue-archive.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export const snapshot = {};

snapshot[`Issue Archive Command - Help Text 1`] = `
stdout:
"
Usage: archive [issueId]

Description:

Archive an issue

Options:

-h, --help - Show this help.
-y, --confirm - Skip confirmation prompt

"
stderr:
""
`;

snapshot[`Issue Archive Command - With Confirm 1`] = `
stdout:
"✓ Successfully archived issue: ENG-123: Archive this issue
"
stderr:
""
`;

snapshot[`Issue Archive Command - Requires Confirmation 1`] = `
stdout:
""
stderr:
"✗ Failed to archive issue: Interactive confirmation required
Use --confirm to skip.
"
`;

snapshot[`Issue Archive Command - Issue Not Found 1`] = `
stdout:
""
stderr:
"✗ Failed to archive issue: Issue not found: ENG-404
"
`;
128 changes: 128 additions & 0 deletions test/commands/issue/issue-archive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { snapshotTest } from "@cliffy/testing"
import { archiveCommand } from "../../../src/commands/issue/issue-archive.ts"
import { MockLinearServer } from "../../utils/mock_linear_server.ts"
import { commonDenoArgs } from "../../utils/test-helpers.ts"

await snapshotTest({
name: "Issue Archive Command - Help Text",
meta: import.meta,
colors: false,
args: ["--help"],
denoArgs: commonDenoArgs,
async fn() {
await archiveCommand.parse()
},
})

await snapshotTest({
name: "Issue Archive Command - With Confirm",
meta: import.meta,
colors: false,
args: ["eng-123", "--confirm"],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
{
queryName: "GetIssueArchiveDetails",
variables: { id: "ENG-123" },
response: {
data: {
issue: {
identifier: "ENG-123",
title: "Archive this issue",
},
},
},
},
{
queryName: "ArchiveIssue",
queryIncludes: "issueArchive(id: $id)",
variables: { id: "ENG-123" },
response: {
data: {
issueArchive: { success: true },
},
},
},
])

try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")

await archiveCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})

await snapshotTest({
name: "Issue Archive Command - Requires Confirmation",
meta: import.meta,
colors: false,
canFail: true,
args: ["ENG-123"],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
{
queryName: "GetIssueArchiveDetails",
variables: { id: "ENG-123" },
response: {
data: {
issue: {
identifier: "ENG-123",
title: "Archive this issue",
},
},
},
},
])

try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")

await archiveCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})

await snapshotTest({
name: "Issue Archive Command - Issue Not Found",
meta: import.meta,
colors: false,
canFail: true,
args: ["ENG-404", "--confirm"],
denoArgs: commonDenoArgs,
async fn() {
const server = new MockLinearServer([
{
queryName: "GetIssueArchiveDetails",
variables: { id: "ENG-404" },
response: { data: { issue: null } },
},
])

try {
await server.start()
Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint())
Deno.env.set("LINEAR_API_KEY", "Bearer test-token")

await archiveCommand.parse()
} finally {
await server.stop()
Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT")
Deno.env.delete("LINEAR_API_KEY")
}
},
})
Loading