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.
- package/CHANGELOG.md +7 -0
- package/COVERAGE.md +26 -25
- package/README.md +163 -2
- package/package.json +1 -1
- package/skills/linear-sdk-cli/SKILL.md +98 -0
- package/skills/linear-sdk-cli/references/auth.md +31 -0
- package/skills/linear-sdk-cli/references/config.md +13 -9
- package/skills/linear-sdk-cli/references/issue.md +3 -2
- package/skills/linear-sdk-cli/references/release-note.md +148 -0
- package/skills/linear-sdk-cli/references/release-pipeline.md +137 -0
- package/skills/linear-sdk-cli/references/release-stage.md +121 -0
- package/skills/linear-sdk-cli/references/release.md +348 -0
- package/src/cli.ts +8 -0
- package/src/commands/issue.ts +9 -3
- package/src/commands/meta.ts +82 -2
- package/src/commands/release-note.ts +90 -0
- package/src/commands/release-pipeline.ts +122 -0
- package/src/commands/release-stage.ts +100 -0
- package/src/commands/release.ts +254 -0
- package/src/config.ts +254 -6
- package/src/lib/keyring.ts +28 -0
- package/src/lib/options.ts +18 -0
- package/src/lib/output-shapes.ts +68 -1
- package/src/lib/release-input.ts +138 -0
- package/src/services/issue.ts +12 -3
- package/src/services/release.ts +691 -0
|
@@ -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
|
+
}
|