linear-sdk-cli 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ import { Command, Option } from "commander";
2
+ import { action } from "../lib/action.js";
3
+ import { booleanValue, repeat, PIPELINE_TYPES } from "../lib/release-input.js";
4
+ import * as svc from "../services/release.js";
5
+
6
+ function options(command: Command, creating = false): Command {
7
+ const result = command
8
+ .option("--name <name>", "pipeline name")
9
+ .addOption(new Option("--type <type>", "pipeline type").choices(PIPELINE_TYPES))
10
+ .option(
11
+ "--production <boolean>",
12
+ "whether pipeline targets production (true|false)",
13
+ booleanValue,
14
+ )
15
+ .option(
16
+ "--auto-release-notes <boolean>",
17
+ "automatically generate notes on completion (true|false)",
18
+ booleanValue,
19
+ )
20
+ .option(
21
+ "--rollover-issues <boolean>",
22
+ "move open issues to the next scheduled release on completion (true|false)",
23
+ booleanValue,
24
+ )
25
+ .addOption(
26
+ new Option("--include-path <glob>", "replace path patterns with these globs (repeatable)")
27
+ .argParser(repeat)
28
+ .conflicts("clearPaths"),
29
+ )
30
+ .addOption(new Option("--clear-paths", "clear all path filters").conflicts("includePath"))
31
+ .addOption(
32
+ new Option("--team-id <uuid>", "replace associated teams with these UUIDs (repeatable)")
33
+ .argParser(repeat)
34
+ .conflicts("clearTeams"),
35
+ )
36
+ .addOption(new Option("--clear-teams", "clear team associations").conflicts("teamId"));
37
+ if (creating)
38
+ for (const flag of ["--name", "--type"])
39
+ result.options.find((o) => o.long === flag)!.makeOptionMandatory();
40
+ return result;
41
+ }
42
+ function input(opts: Record<string, any>): svc.PipelineInput {
43
+ return {
44
+ name: opts.name,
45
+ type: opts.type,
46
+ isProduction: opts.production,
47
+ autoGenerateReleaseNotesOnCompletion: opts.autoReleaseNotes,
48
+ rolloverIssuesOnCompletion: opts.rolloverIssues,
49
+ includePathPatterns: opts.clearPaths ? [] : opts.includePath,
50
+ teamIds: opts.clearTeams ? [] : opts.teamId,
51
+ };
52
+ }
53
+ export function registerReleasePipeline(program: Command): void {
54
+ const group = program
55
+ .command("release-pipeline")
56
+ .description("Manage release pipelines (Business/Enterprise)")
57
+ .addHelpText(
58
+ "after",
59
+ "\nUse normal Linear OAuth/API credentials. Pipeline access keys belong to\nlinear-release and cannot replace these credentials. Creating a pipeline\ncreates default stages; entitlement and workspace permissions are enforced by Linear.",
60
+ );
61
+ group
62
+ .command("list")
63
+ .alias("ls")
64
+ .description("List workspace release pipelines")
65
+ .option("--include-archived", "include archived pipelines")
66
+ .action(
67
+ action(async (ctx, opts) => {
68
+ const rows = await svc.listPipelines(ctx.client, ctx.limit, opts.includeArchived);
69
+ ctx.output.list(
70
+ rows,
71
+ [
72
+ { key: "id", header: "ID", value: (r) => r.id },
73
+ { key: "name", header: "Name", value: (r) => r.name },
74
+ { key: "type", header: "Type", value: (r) => r.type },
75
+ { key: "slugId", header: "Slug", value: (r) => r.slugId },
76
+ ],
77
+ rows,
78
+ );
79
+ }),
80
+ );
81
+ group
82
+ .command("view <id>")
83
+ .description("Show pipeline settings by UUID or slug")
84
+ .action(
85
+ action(async (ctx, _opts, id: string) => {
86
+ const p = await svc.getPipeline(ctx.client, id);
87
+ ctx.output.detail(p, [
88
+ ["ID", p.id],
89
+ ["Name", p.name],
90
+ ["Slug", p.slugId],
91
+ ["Type", p.type],
92
+ ["Production", p.isProduction],
93
+ ["Auto release notes", p.autoGenerateReleaseNotesOnCompletion],
94
+ ["Rollover issues", p.rolloverIssuesOnCompletion],
95
+ ["Path patterns", p.includePathPatterns.join(", ")],
96
+ ["Archived", p.archivedAt],
97
+ ["URL", p.url],
98
+ ]);
99
+ }),
100
+ );
101
+ options(
102
+ group.command("create").description("Create a pipeline with default stages"),
103
+ true,
104
+ ).action(
105
+ action(async (ctx, opts) => {
106
+ const p = await svc.createPipeline(ctx.client, {
107
+ ...input(opts),
108
+ name: opts.name,
109
+ type: opts.type,
110
+ });
111
+ ctx.output.emit(p, () => ctx.output.success(`Created release pipeline ${p.name} (${p.id})`));
112
+ }),
113
+ );
114
+ options(
115
+ group.command("update <id>").description("Update pipeline settings by UUID or slug"),
116
+ ).action(
117
+ action(async (ctx, opts, id: string) => {
118
+ const p = await svc.updatePipeline(ctx.client, id, input(opts));
119
+ ctx.output.emit(p, () => ctx.output.success(`Updated release pipeline ${p.name} (${p.id})`));
120
+ }),
121
+ );
122
+ }
@@ -0,0 +1,100 @@
1
+ import { Command, Option } from "commander";
2
+ import { action } from "../lib/action.js";
3
+ import { booleanValue, finiteNumber, STAGE_TYPES } from "../lib/release-input.js";
4
+ import * as svc from "../services/release.js";
5
+
6
+ function options(command: Command, creating = false): Command {
7
+ const result = command
8
+ .option("--name <name>", "stage name")
9
+ .option("--color <hex>", "six-digit HEX color")
10
+ .option("--position <number>", "stage ordering position", finiteNumber)
11
+ .option(
12
+ "--frozen <boolean>",
13
+ "exclude this started stage from automated sync (true|false)",
14
+ booleanValue,
15
+ );
16
+ if (creating)
17
+ for (const flag of ["--name", "--color", "--position"])
18
+ result.options.find((o) => o.long === flag)!.makeOptionMandatory();
19
+ return result;
20
+ }
21
+ function input(opts: Record<string, any>): svc.StageInput {
22
+ return { name: opts.name, color: opts.color, position: opts.position, frozen: opts.frozen };
23
+ }
24
+ export function registerReleaseStage(program: Command): void {
25
+ const group = program
26
+ .command("release-stage")
27
+ .description("Manage release stages and freeze scheduled release scope")
28
+ .addHelpText(
29
+ "after",
30
+ "\nOnly started stages can be frozen. Frozen stages exclude their releases from\nautomated sync; at least one started stage must remain unfrozen. Stage types\nare immutable. Linear enforces pipeline stage invariants.",
31
+ );
32
+ group
33
+ .command("list <pipeline>")
34
+ .alias("ls")
35
+ .description("List stages for a pipeline UUID or slug")
36
+ .option("--include-archived", "include archived stages")
37
+ .action(
38
+ action(async (ctx, opts, pipeline: string) => {
39
+ const rows = await svc.listStages(ctx.client, pipeline, ctx.limit, opts.includeArchived);
40
+ ctx.output.list(
41
+ rows,
42
+ [
43
+ { key: "id", header: "ID", value: (r) => r.id },
44
+ { key: "name", header: "Name", value: (r) => r.name },
45
+ { key: "type", header: "Type", value: (r) => r.type },
46
+ { key: "position", header: "Position", value: (r) => r.position },
47
+ { key: "frozen", header: "Frozen", value: (r) => r.frozen },
48
+ ],
49
+ rows,
50
+ );
51
+ }),
52
+ );
53
+ group
54
+ .command("view <id>")
55
+ .description("Show a release stage by UUID")
56
+ .action(
57
+ action(async (ctx, _opts, id: string) => {
58
+ const s = await svc.getStage(ctx.client, id);
59
+ ctx.output.detail(s, [
60
+ ["ID", s.id],
61
+ ["Name", s.name],
62
+ ["Type", s.type],
63
+ ["Color", s.color],
64
+ ["Position", s.position],
65
+ ["Frozen", s.frozen],
66
+ ["Pipeline", s.pipeline.name],
67
+ ["Archived", s.archivedAt],
68
+ ]);
69
+ }),
70
+ );
71
+ options(group.command("create").description("Create a stage in a release pipeline"), true)
72
+ .requiredOption("--pipeline <id|slug>", "pipeline UUID or slug")
73
+ .addOption(
74
+ new Option("--type <type>", "lifecycle stage type")
75
+ .choices(STAGE_TYPES)
76
+ .makeOptionMandatory(),
77
+ )
78
+ .action(
79
+ action(async (ctx, opts) => {
80
+ const s = await svc.createStage(ctx.client, opts.pipeline, {
81
+ ...input(opts),
82
+ name: opts.name,
83
+ color: opts.color,
84
+ position: opts.position,
85
+ type: opts.type,
86
+ });
87
+ ctx.output.emit(s, () => ctx.output.success(`Created release stage ${s.name} (${s.id})`));
88
+ }),
89
+ );
90
+ options(
91
+ group
92
+ .command("update <id>")
93
+ .description("Update name, color, order, or frozen state by stage UUID"),
94
+ ).action(
95
+ action(async (ctx, opts, id: string) => {
96
+ const s = await svc.updateStage(ctx.client, id, input(opts));
97
+ ctx.output.emit(s, () => ctx.output.success(`Updated release stage ${s.name} (${s.id})`));
98
+ }),
99
+ );
100
+ }
@@ -0,0 +1,254 @@
1
+ import { Command, Option } from "commander";
2
+ import { action } from "../lib/action.js";
3
+ import {
4
+ descriptionOptions,
5
+ releaseInput,
6
+ lifecycleOptions,
7
+ lifecycleInput,
8
+ issueReferences,
9
+ repeat,
10
+ STAGE_TYPES,
11
+ } from "../lib/release-input.js";
12
+ import { confirmDestructive } from "../lib/prompt.js";
13
+ import * as svc from "../services/release.js";
14
+ import type { Context } from "../context.js";
15
+ import type { Column } from "../output/table.js";
16
+
17
+ const COLUMNS: Column<svc.ReleaseRow>[] = [
18
+ { key: "id", header: "ID", value: (r) => r.id },
19
+ { key: "name", header: "Name", value: (r) => r.name },
20
+ { key: "version", header: "Version", value: (r) => r.version ?? "—" },
21
+ { key: "pipeline", header: "Pipeline", value: (r) => r.pipeline.name },
22
+ { key: "stage", header: "Stage", value: (r) => r.stage.name },
23
+ ];
24
+ function emit(ctx: Context, value: svc.ReleaseDetail, verb: string): void {
25
+ ctx.output.emit(value, () => ctx.output.success(`${verb} release ${value.name} (${value.id})`));
26
+ }
27
+ function editOptions(command: Command, creating = false): Command {
28
+ const result = descriptionOptions(command)
29
+ .option("--name <name>", "release name")
30
+ .option("--release-version <version>", "release version (unique within the pipeline)")
31
+ .option("--commit-sha <sha>", "full Git commit SHA")
32
+ .option("--stage <id>", "stage UUID from the same pipeline")
33
+ .option("--start-date <date>", "estimated start date (YYYY-MM-DD)")
34
+ .option("--target-date <date>", "estimated completion date (YYYY-MM-DD)");
35
+ if (creating) result.options.find((o) => o.long === "--name")!.makeOptionMandatory();
36
+ return result;
37
+ }
38
+ export function registerRelease(program: Command): void {
39
+ const group = program
40
+ .command("release")
41
+ .description("Track releases and issue membership (Business/Enterprise)")
42
+ .addHelpText(
43
+ "after",
44
+ "\nUse pipeline UUIDs/slugs and explicit versions for lifecycle operations.\nGit scanning and pipeline access keys: https://github.com/linear/linear-release\nCompleting a scheduled release may move open issues to the next release.",
45
+ );
46
+ group
47
+ .command("list")
48
+ .alias("ls")
49
+ .description("List releases, optionally scoped to a pipeline")
50
+ .option("--pipeline <id|slug>", "filter by pipeline UUID or slug")
51
+ .option("--release-version <version>", "filter by exact version")
52
+ .addOption(
53
+ new Option("--stage-type <type>", "filter by lifecycle stage type").choices(STAGE_TYPES),
54
+ )
55
+ .option("--include-archived", "include archived releases")
56
+ .action(
57
+ action(async (ctx, opts) => {
58
+ const rows = await svc.listReleases(ctx.client, ctx.limit, {
59
+ pipeline: opts.pipeline,
60
+ version: opts.releaseVersion,
61
+ stageType: opts.stageType,
62
+ includeArchived: opts.includeArchived,
63
+ });
64
+ ctx.output.list(rows, COLUMNS, rows);
65
+ }),
66
+ );
67
+ group
68
+ .command("view <id>")
69
+ .description("Show a release by UUID or slug")
70
+ .action(
71
+ action(async (ctx, _opts, id: string) => {
72
+ const r = await svc.getRelease(ctx.client, id);
73
+ ctx.output.detail(r, [
74
+ ["ID", r.id],
75
+ ["Name", r.name],
76
+ ["Version", r.version],
77
+ ["Commit SHA", r.commitSha],
78
+ ["Pipeline", r.pipeline.name],
79
+ ["Stage", r.stage.name],
80
+ ["Start date", r.startDate],
81
+ ["Target date", r.targetDate],
82
+ ["Started", r.startedAt],
83
+ ["Completed", r.completedAt],
84
+ ["Canceled", r.canceledAt],
85
+ ["Archived", r.archivedAt],
86
+ ["Issues", r.issueCount],
87
+ ["URL", r.url],
88
+ ["Description", r.description],
89
+ ]);
90
+ }),
91
+ );
92
+ editOptions(
93
+ group
94
+ .command("create")
95
+ .alias("new")
96
+ .description("Create a release (stage defaults depend on pipeline type)"),
97
+ true,
98
+ )
99
+ .requiredOption("--pipeline <id|slug>", "pipeline UUID or slug")
100
+ .action(
101
+ action(async (ctx, opts) =>
102
+ emit(
103
+ ctx,
104
+ await svc.createRelease(ctx.client, opts.pipeline, {
105
+ ...releaseInput(opts),
106
+ name: opts.name,
107
+ }),
108
+ "Created",
109
+ ),
110
+ ),
111
+ );
112
+ editOptions(
113
+ group
114
+ .command("update <id>")
115
+ .alias("edit")
116
+ .description("Update exact release metadata or stage by UUID/slug"),
117
+ ).action(
118
+ action(async (ctx, opts, id: string) =>
119
+ emit(ctx, await svc.updateRelease(ctx.client, id, releaseInput(opts)), "Updated"),
120
+ ),
121
+ );
122
+ lifecycleOptions(
123
+ group
124
+ .command("sync")
125
+ .description("Sync explicit issue references; continuous pipelines complete immediately"),
126
+ )
127
+ .requiredOption("--commit-sha <sha>", "full Git commit SHA (no implicit checkout scanning)")
128
+ .option(
129
+ "--issue <key[=sha]>",
130
+ "issue reference, defaulting to --commit-sha (repeatable)",
131
+ repeat,
132
+ )
133
+ .option(
134
+ "--reverted-issue <key[=sha]>",
135
+ "reverted issue reference to remove (repeatable)",
136
+ repeat,
137
+ )
138
+ .addHelpText(
139
+ "after",
140
+ "\nScheduled sync may label an unversioned started release or create a release.\nIssue references accumulate; omitted issues are not removed. Use --reverted-issue\nor release remove-issue for removal. Use linear-release for Git/PR attribution.",
141
+ )
142
+ .action(
143
+ action(async (ctx, opts) =>
144
+ emit(
145
+ ctx,
146
+ await svc.syncRelease(ctx.client, opts.pipeline, opts.releaseVersion, {
147
+ ...lifecycleInput(opts),
148
+ commitSha: opts.commitSha,
149
+ issueReferences: issueReferences(opts.issue, opts.commitSha),
150
+ revertedIssueReferences: issueReferences(opts.revertedIssue, opts.commitSha),
151
+ }),
152
+ "Synced",
153
+ ),
154
+ ),
155
+ );
156
+ lifecycleOptions(
157
+ group
158
+ .command("update-version")
159
+ .description("Update stage/name/links for an explicit pipeline and release version"),
160
+ )
161
+ .option("--stage <name>", "stage name (Linear matches exact name, then normalized name)")
162
+ .action(
163
+ action(async (ctx, opts) =>
164
+ emit(
165
+ ctx,
166
+ await svc.updateReleaseVersion(ctx.client, opts.pipeline, opts.releaseVersion, {
167
+ ...lifecycleInput(opts),
168
+ stage: opts.stage,
169
+ }),
170
+ "Updated",
171
+ ),
172
+ ),
173
+ );
174
+ lifecycleOptions(
175
+ group
176
+ .command("complete")
177
+ .description("Complete an explicit scheduled release; may roll open issues forward"),
178
+ )
179
+ .option(
180
+ "--commit-sha <sha>",
181
+ "full SHA (Linear preserves an existing SHA when targeting a version)",
182
+ )
183
+ .addHelpText(
184
+ "after",
185
+ "\nComplete only after your deployment and verification gates pass. This command\nrecords lifecycle state; it does not deploy or verify code.",
186
+ )
187
+ .action(
188
+ action(async (ctx, opts) =>
189
+ emit(
190
+ ctx,
191
+ await svc.completeRelease(ctx.client, opts.pipeline, opts.releaseVersion, {
192
+ ...lifecycleInput(opts),
193
+ commitSha: opts.commitSha,
194
+ }),
195
+ "Completed",
196
+ ),
197
+ ),
198
+ );
199
+ group
200
+ .command("issues <id>")
201
+ .description("List issues in a release (UUID or slug)")
202
+ .action(
203
+ action(async (ctx, _opts, id: string) => {
204
+ const rows = await svc.listReleaseIssues(ctx.client, id, ctx.limit);
205
+ ctx.output.list(
206
+ rows,
207
+ [
208
+ { key: "identifier", header: "Issue", value: (r) => r.identifier },
209
+ { key: "title", header: "Title", value: (r) => r.title },
210
+ ],
211
+ rows,
212
+ );
213
+ }),
214
+ );
215
+ group
216
+ .command("links <id>")
217
+ .description("List external links on a release (UUID or slug)")
218
+ .action(
219
+ action(async (ctx, _opts, id: string) => {
220
+ const rows = await svc.listReleaseLinks(ctx.client, id, ctx.limit);
221
+ ctx.output.list(
222
+ rows,
223
+ [
224
+ { key: "label", header: "Label", value: (r) => r.label ?? "—" },
225
+ { key: "url", header: "URL", value: (r) => r.url },
226
+ ],
227
+ rows,
228
+ );
229
+ }),
230
+ );
231
+ group
232
+ .command("add-issue <id> <issue>")
233
+ .description("Add an issue (KEY-123 or UUID) to an exact release")
234
+ .action(
235
+ action(async (ctx, _opts, id: string, issue: string) => {
236
+ const receipt = await svc.addIssue(ctx.client, id, issue);
237
+ ctx.output.emit(receipt, () =>
238
+ ctx.output.success(`Added ${receipt.identifier} to release ${receipt.releaseId}`),
239
+ );
240
+ }),
241
+ );
242
+ group
243
+ .command("remove-issue <id> <issue>")
244
+ .description("Remove an issue from an exact release")
245
+ .action(
246
+ action(async (ctx, _opts, id: string, issue: string) => {
247
+ if (!(await confirmDestructive(ctx, `Remove ${issue} from release ${id}?`))) return;
248
+ const receipt = await svc.removeIssue(ctx.client, id, issue);
249
+ ctx.output.emit(receipt, () =>
250
+ ctx.output.success(`Removed ${issue} from release ${receipt.releaseId}`),
251
+ );
252
+ }),
253
+ );
254
+ }