Skip to content
Draft
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ linear-release sync

# Specify custom name and version
linear-release sync --name="Release 1.2.0" --release-version="1.2.0"

# Set release metadata
linear-release sync --release-version="1.2.0" --description="Highlights and rollout details"
```

### `complete`
Expand All @@ -124,6 +127,9 @@ linear-release complete --release-version="1.2.0"

# Sets a custom name when completing the release
linear-release complete --name="Release 1.2.0"

# Sets the description when completing the release
linear-release complete --description="Highlights and rollout details"
```

### `update`
Expand All @@ -139,6 +145,9 @@ linear-release update --stage="in review" --release-version="1.2.0"

# Sets a custom name when updating the release
linear-release update --stage="in review" --name="Release 1.2.0"

# Sets the description when updating the release
linear-release update --stage="in review" --description="Highlights and rollout details"
```

## Configuration
Expand All @@ -158,6 +167,7 @@ The provider is detected from the remote hostname, or on GitLab CI from the job
| ---------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--name` | `sync`, `complete`, `update` | Custom release name. For `sync`, the value is applied to the targeted release — both newly created releases and existing ones get the provided name. For `complete` and `update`, sets the name on the targeted release. |
| `--release-version` | `sync`, `complete`, `update` | Release version identifier. For `sync`, defaults to short commit hash. For `complete` and `update`, selects an existing release with that version (errors if none exists); does not change a release's version. If omitted, targets the most recent started release. |
| `--description` | `sync`, `complete`, `update` | Release description. Pass an empty string to clear an existing description. |
| `--stage` | `update` | Target deployment stage (required for `update`) |
| `--include-paths` | `sync` | Filter commits by changed file paths |
| `--include-subjects` | `sync` | Filter commits whose subject (first line) matches a regex |
Expand Down
10 changes: 10 additions & 0 deletions src/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ describe("parseCLIArgs", () => {
expect(result.releaseName).toBe("Release 1.2.0");
});

it("parses --description", () => {
const result = parseCLIArgs(["--description", "Android + iOS; Shorebird patch 3"]);
expect(result.releaseDescription).toBe("Android + iOS; Shorebird patch 3");
});

it("preserves an empty --description", () => {
const result = parseCLIArgs(["--description", ""]);
expect(result.releaseDescription).toBe("");
});

it("parses --stage", () => {
const result = parseCLIArgs(["--stage", "production"]);
expect(result.stageName).toBe("production");
Expand Down
3 changes: 3 additions & 0 deletions src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type ParsedCLIArgs = {
command: string;
releaseName?: string;
releaseVersion?: string;
releaseDescription?: string;
stageName?: string;
baseRef?: string;
includePaths: string[];
Expand Down Expand Up @@ -130,6 +131,7 @@ export function parseCLIArgs(argv: string[]): ParsedCLIArgs {
options: {
name: { type: "string" },
"release-version": { type: "string" },
description: { type: "string" },
stage: { type: "string" },
"base-ref": { type: "string" },
"include-paths": { type: "string" },
Expand Down Expand Up @@ -236,6 +238,7 @@ export function parseCLIArgs(argv: string[]): ParsedCLIArgs {
command,
releaseName: values.name,
releaseVersion: values["release-version"],
releaseDescription: values.description,
stageName: values.stage,
baseRef: values["base-ref"],
includePaths: values["include-paths"]
Expand Down
39 changes: 32 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Commands:
Options:
--name=<name> Custom release name
--release-version=<version> Release version identifier
--description=<description> Release description (empty string clears it)
--stage=<stage> Deployment stage (required for update)
--include-paths=<paths> Filter commits by file paths (comma-separated globs)
--include-subjects=<regex> Filter commits whose subject (first line) matches the regex
Expand All @@ -90,6 +91,7 @@ Environment:
Examples:
linear-release sync
linear-release sync --name="Release 1.2.0" --release-version="1.2.0"
linear-release sync --release-version="1.2.0" --description="Android + iOS"
linear-release complete
linear-release update --stage=production
linear-release sync --include-paths="apps/web/**,packages/**"
Expand Down Expand Up @@ -123,6 +125,7 @@ const {
command,
releaseName,
releaseVersion,
releaseDescription,
stageName,
baseRef,
includePaths,
Expand Down Expand Up @@ -223,6 +226,10 @@ function formatReleaseNotesSummary(notes: ReleaseNotes | undefined): string {
return notes ? `, release notes (${notes.content.length} chars)` : "";
}

function formatDescriptionSummary(description: string | undefined): string {
return description !== undefined ? `, description (${description.length} chars)` : "";
}

const logEnvironmentSummary = () => {
info(`linear-release v${getCliVersion()}`);
if (releaseName) {
Expand Down Expand Up @@ -402,7 +409,7 @@ async function syncCommand(): Promise<{
const targetName = releaseName ?? "(server-assigned)";
const versionPart = releaseVersion ? `version: ${releaseVersion}` : "no version set";
info(
`[dry-run] Would sync release ${targetName} (${versionPart}): ${scanned}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
`[dry-run] Would sync release ${targetName} (${versionPart}): ${scanned}${formatDescriptionSummary(releaseDescription)}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
);
return null;
}
Expand All @@ -416,9 +423,10 @@ async function syncCommand(): Promise<{
links,
documents,
releaseNotes,
releaseDescription,
);
info(
`Synced to release ${release.name} (${formatVersion(release)}): ${scanned}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
`Synced to release ${release.name} (${formatVersion(release)}): ${scanned}${formatDescriptionSummary(releaseDescription)}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
);
if (scanBase.kind === "base-ref") {
info(`Stored release baseline: ${(release.commitSha ?? currentCommit.commit).slice(0, 7)}`);
Expand Down Expand Up @@ -446,7 +454,7 @@ async function completeCommand(): Promise<{
const targetName = releaseName ?? "(current release)";
const versionPart = releaseVersion ? `version: ${releaseVersion}` : "no version set";
info(
`[dry-run] Would complete release ${targetName} (${versionPart})${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
`[dry-run] Would complete release ${targetName} (${versionPart})${formatDescriptionSummary(releaseDescription)}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
);
return null;
}
Expand All @@ -458,10 +466,11 @@ async function completeCommand(): Promise<{
links,
documents,
releaseNotes,
description: releaseDescription,
});
if (result.success) {
info(
`Completed release ${result.release?.name ?? "(unknown)"} (${formatVersion(result.release)})${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
`Completed release ${result.release?.name ?? "(unknown)"} (${formatVersion(result.release)})${formatDescriptionSummary(releaseDescription)}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
);
} else {
throw new Error("Failed to complete release");
Expand Down Expand Up @@ -492,7 +501,7 @@ async function updateCommand(): Promise<{
const targetName = releaseName ?? "(current release)";
const versionPart = releaseVersion ? `version: ${releaseVersion}` : "no version set";
info(
`[dry-run] Would update release ${targetName} (${versionPart}) to stage ${stageName}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
`[dry-run] Would update release ${targetName} (${versionPart}) to stage ${stageName}${formatDescriptionSummary(releaseDescription)}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
);
return null;
}
Expand All @@ -503,6 +512,7 @@ async function updateCommand(): Promise<{
stage: stageName,
version: releaseVersion,
name: releaseName,
description: releaseDescription,
links,
documents,
releaseNotes,
Expand All @@ -514,7 +524,7 @@ async function updateCommand(): Promise<{

if (result.success) {
info(
`Updated release ${result.release?.name ?? "(unknown)"} (${formatVersion(result.release)}) to stage ${result.release?.stageName}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
`Updated release ${result.release?.name ?? "(unknown)"} (${formatVersion(result.release)}) to stage ${result.release?.stageName}${formatDescriptionSummary(releaseDescription)}${formatLinkSummary(links)}${formatDocumentsSummary(documents)}${formatReleaseNotesSummary(releaseNotes)}`,
);
} else {
throw new Error("Failed to update release");
Expand Down Expand Up @@ -611,6 +621,7 @@ async function syncRelease(
releaseLinks: ReleaseLink[],
releaseDocuments: ReleaseDocument[],
releaseNotesValue: ReleaseNotes | undefined,
description: string | undefined,
): Promise<Release> {
const currentSha = await getCurrentGitInfo().commit;
if (!currentSha) {
Expand Down Expand Up @@ -643,6 +654,7 @@ async function syncRelease(
input: {
name: releaseName,
version: releaseVersion,
description,
commitSha: currentSha,
issueReferences,
revertedIssueReferences: revertedIssueReferences.length > 0 ? revertedIssueReferences : undefined,
Expand Down Expand Up @@ -681,6 +693,7 @@ async function completeRelease(options: {
links: ReleaseLink[];
documents: ReleaseDocument[];
releaseNotes?: ReleaseNotes;
description?: string;
}): Promise<{
success: boolean;
release: { id: string; name: string; version?: string; url?: string } | null;
Expand All @@ -692,6 +705,7 @@ async function completeRelease(options: {
links: releaseLinks,
documents: releaseDocuments,
releaseNotes: notesValue,
description,
} = options;

const response = await apiRequest<AccessKeyCompleteReleaseResponse>(
Expand All @@ -712,6 +726,7 @@ async function completeRelease(options: {
input: {
name,
version,
description,
commitSha,
links: releaseLinks.length > 0 ? releaseLinks : undefined,
documents: releaseDocuments.length > 0 ? releaseDocuments : undefined,
Expand All @@ -727,6 +742,7 @@ async function updateReleaseByPipeline(options: {
stage?: string;
version?: string;
name?: string;
description?: string;
links: ReleaseLink[];
documents: ReleaseDocument[];
releaseNotes?: ReleaseNotes;
Expand All @@ -740,7 +756,15 @@ async function updateReleaseByPipeline(options: {
stageName: string;
} | null;
}> {
const { stage, version, name, links: releaseLinks, documents: releaseDocuments, releaseNotes: notesValue } = options;
const {
stage,
version,
name,
description,
links: releaseLinks,
documents: releaseDocuments,
releaseNotes: notesValue,
} = options;
const response = await apiRequest<AccessKeyUpdateByPipelineResponse>(
`
mutation releaseUpdateByPipelineByAccessKey($input: ReleaseUpdateByPipelineInputBase!) {
Expand All @@ -763,6 +787,7 @@ async function updateReleaseByPipeline(options: {
stage,
version,
name,
description,
links: releaseLinks.length > 0 ? releaseLinks : undefined,
documents: releaseDocuments.length > 0 ? releaseDocuments : undefined,
releaseNotes: notesValue,
Expand Down
Loading