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,138 @@
|
|
|
1
|
+
/** Parsing shared by the release command groups; API behavior stays in services. */
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
|
+
import { resolveBody } from "./body.js";
|
|
4
|
+
import { usageError } from "./errors.js";
|
|
5
|
+
import type { LifecycleInput, ReleaseInput, NoteInput, LinkInput } from "../services/release.js";
|
|
6
|
+
|
|
7
|
+
export const PIPELINE_TYPES = ["scheduled", "continuous"];
|
|
8
|
+
export const STAGE_TYPES = ["planned", "started", "completed", "canceled"];
|
|
9
|
+
export const repeat = (value: string, previous: string[] = []): string[] => [...previous, value];
|
|
10
|
+
|
|
11
|
+
export function booleanValue(value: string): boolean {
|
|
12
|
+
if (value === "true") return true;
|
|
13
|
+
if (value === "false") return false;
|
|
14
|
+
throw usageError("Expected true or false.");
|
|
15
|
+
}
|
|
16
|
+
export function finiteNumber(value: string): number {
|
|
17
|
+
if (!value.trim() || !Number.isFinite(Number(value)))
|
|
18
|
+
throw usageError("Expected a finite number.");
|
|
19
|
+
return Number(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function descriptionOptions(command: Command): Command {
|
|
23
|
+
return command
|
|
24
|
+
.addOption(
|
|
25
|
+
new Option("--description <text>", "release description (Markdown)").conflicts([
|
|
26
|
+
"descriptionFile",
|
|
27
|
+
"clearDescription",
|
|
28
|
+
]),
|
|
29
|
+
)
|
|
30
|
+
.addOption(
|
|
31
|
+
new Option(
|
|
32
|
+
"--description-file <path>",
|
|
33
|
+
"read description from file ('-' for stdin)",
|
|
34
|
+
).conflicts(["description", "clearDescription"]),
|
|
35
|
+
)
|
|
36
|
+
.addOption(
|
|
37
|
+
new Option("--clear-description", "clear the description").conflicts([
|
|
38
|
+
"description",
|
|
39
|
+
"descriptionFile",
|
|
40
|
+
]),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
export function releaseInput(opts: Record<string, any>): ReleaseInput {
|
|
44
|
+
return {
|
|
45
|
+
name: opts.name,
|
|
46
|
+
version: opts.releaseVersion,
|
|
47
|
+
commitSha: opts.commitSha,
|
|
48
|
+
stageId: opts.stage,
|
|
49
|
+
startDate: opts.startDate,
|
|
50
|
+
targetDate: opts.targetDate,
|
|
51
|
+
description: description(opts),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function description(opts: Record<string, any>): string | null | undefined {
|
|
55
|
+
return opts.clearDescription
|
|
56
|
+
? null
|
|
57
|
+
: resolveBody({ arg: opts.description, file: opts.descriptionFile, interactive: false });
|
|
58
|
+
}
|
|
59
|
+
export function lifecycleOptions(command: Command): Command {
|
|
60
|
+
return descriptionOptions(command)
|
|
61
|
+
.requiredOption("--pipeline <id|slug>", "target pipeline UUID or slug")
|
|
62
|
+
.requiredOption(
|
|
63
|
+
"--release-version <version>",
|
|
64
|
+
"explicit release version within the pipeline (no latest fallback)",
|
|
65
|
+
)
|
|
66
|
+
.option("--name <name>", "release name")
|
|
67
|
+
.option("--link <label=url|url>", "attach an external link (repeatable)", repeat);
|
|
68
|
+
}
|
|
69
|
+
export function lifecycleInput(opts: Record<string, any>): LifecycleInput {
|
|
70
|
+
return {
|
|
71
|
+
name: opts.name,
|
|
72
|
+
description: description(opts),
|
|
73
|
+
links: opts.link?.map(parseReleaseLink),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function parseReleaseLink(value: string): LinkInput {
|
|
77
|
+
// Bare URLs may contain '=' in their query string; do not split those.
|
|
78
|
+
const bare = /^https?:\/\//i.test(value);
|
|
79
|
+
const index = value.indexOf("=");
|
|
80
|
+
const url = bare || index < 0 ? value : value.slice(index + 1);
|
|
81
|
+
const label = bare || index < 0 ? undefined : value.slice(0, index);
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(url);
|
|
84
|
+
if (
|
|
85
|
+
!["https:", "http:"].includes(parsed.protocol) ||
|
|
86
|
+
parsed.username ||
|
|
87
|
+
parsed.password ||
|
|
88
|
+
label === ""
|
|
89
|
+
)
|
|
90
|
+
throw new Error();
|
|
91
|
+
} catch {
|
|
92
|
+
throw usageError("--link requires an HTTP(S) URL or Label=URL, without embedded credentials.");
|
|
93
|
+
}
|
|
94
|
+
return { url, ...(label === undefined ? {} : { label }) };
|
|
95
|
+
}
|
|
96
|
+
export function issueReferences(values: string[] | undefined, commitSha: string) {
|
|
97
|
+
return values?.map((value) => {
|
|
98
|
+
const [identifier, sha = commitSha, ...rest] = value.split("=");
|
|
99
|
+
if (
|
|
100
|
+
!/^[a-z][a-z\d]*-\d+$/i.test(identifier ?? "") ||
|
|
101
|
+
rest.length ||
|
|
102
|
+
!/^(?:[a-f\d]{40}|[a-f\d]{64})$/i.test(sha)
|
|
103
|
+
) {
|
|
104
|
+
throw usageError("--issue/--reverted-issue requires KEY-123 or KEY-123=FULL_SHA.");
|
|
105
|
+
}
|
|
106
|
+
return { identifier: identifier!.toUpperCase(), commitSha: sha };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function noteOptions(command: Command): Command {
|
|
110
|
+
return command
|
|
111
|
+
.option("--title <title>", "release note title")
|
|
112
|
+
.addOption(new Option("--body <text>", "release note body (Markdown)").conflicts("bodyFile"))
|
|
113
|
+
.addOption(
|
|
114
|
+
new Option("--body-file <path>", "read Markdown from file ('-' for stdin)").conflicts("body"),
|
|
115
|
+
)
|
|
116
|
+
.addOption(
|
|
117
|
+
new Option("--release <id>", "covered release UUID or slug (repeatable)")
|
|
118
|
+
.argParser(repeat)
|
|
119
|
+
.conflicts(["fromRelease", "toRelease"]),
|
|
120
|
+
)
|
|
121
|
+
.addOption(
|
|
122
|
+
new Option("--from-release <id>", "oldest release in an inclusive createdAt range").conflicts(
|
|
123
|
+
"release",
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
.addOption(
|
|
127
|
+
new Option("--to-release <id>", "newest release in the inclusive range").conflicts("release"),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
export function noteInput(opts: Record<string, any>): NoteInput {
|
|
131
|
+
return {
|
|
132
|
+
title: opts.title,
|
|
133
|
+
content: resolveBody({ arg: opts.body, file: opts.bodyFile, interactive: false }),
|
|
134
|
+
releaseIds: opts.release,
|
|
135
|
+
rangeFromReleaseId: opts.fromRelease,
|
|
136
|
+
rangeToReleaseId: opts.toRelease,
|
|
137
|
+
};
|
|
138
|
+
}
|
package/src/services/issue.ts
CHANGED
|
@@ -922,7 +922,10 @@ export interface CreateOptions {
|
|
|
922
922
|
state?: string;
|
|
923
923
|
priority?: number;
|
|
924
924
|
label?: string[];
|
|
925
|
-
project
|
|
925
|
+
/** Explicit project; null creates outside any project, including template/parent defaults. */
|
|
926
|
+
project?: string | null;
|
|
927
|
+
/** Configured fallback, applied only when neither an explicit nor a parent project wins. */
|
|
928
|
+
defaultProject?: string;
|
|
926
929
|
milestone?: string;
|
|
927
930
|
cycle?: string;
|
|
928
931
|
estimate?: number;
|
|
@@ -1305,6 +1308,7 @@ export async function prepareIssueCreate(
|
|
|
1305
1308
|
if (opts.clearDelegate) input.delegateId = null;
|
|
1306
1309
|
if (opts.state) input.stateId = await resolveStateId(client, team.id, opts.state);
|
|
1307
1310
|
if (opts.label?.length) input.labelIds = await resolveLabelIds(client, opts.label, team.id);
|
|
1311
|
+
if (opts.project === null) input.projectId = null;
|
|
1308
1312
|
if (opts.project) input.projectId = await resolveProjectId(client, opts.project);
|
|
1309
1313
|
if (opts.parent) {
|
|
1310
1314
|
const parent = await resolveIssue(client, opts.parent);
|
|
@@ -1312,15 +1316,20 @@ export async function prepareIssueCreate(
|
|
|
1312
1316
|
// A sub-issue lands in its parent's project unless told otherwise — what
|
|
1313
1317
|
// Linear's UI does, and what the reference CLI does. Without this, every
|
|
1314
1318
|
// `--parent` child sat outside the project its parent belongs to.
|
|
1315
|
-
if (
|
|
1319
|
+
if (input.projectId === undefined) {
|
|
1316
1320
|
const parentProject = await parent.project;
|
|
1317
1321
|
if (parentProject) input.projectId = parentProject.id;
|
|
1318
1322
|
}
|
|
1319
1323
|
}
|
|
1324
|
+
if (input.projectId === undefined && opts.defaultProject) {
|
|
1325
|
+
input.projectId = await resolveProjectId(client, opts.defaultProject);
|
|
1326
|
+
}
|
|
1320
1327
|
if (opts.milestone) {
|
|
1321
1328
|
// Resolved after the parent so a milestone can name one in the inherited project.
|
|
1322
1329
|
if (!input.projectId)
|
|
1323
|
-
throw usageError(
|
|
1330
|
+
throw usageError(
|
|
1331
|
+
"A milestone requires --project, a configured project, or a --parent in a project.",
|
|
1332
|
+
);
|
|
1324
1333
|
input.projectMilestoneId = await resolveMilestoneId(client, input.projectId, opts.milestone);
|
|
1325
1334
|
}
|
|
1326
1335
|
if (opts.cycle) input.cycleId = await resolveCycleId(client, team.id, opts.cycle);
|