diff --git a/README.md b/README.md index bb7faea..ab4141f 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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` @@ -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 @@ -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 | diff --git a/src/args.test.ts b/src/args.test.ts index e122147..aa9b688 100644 --- a/src/args.test.ts +++ b/src/args.test.ts @@ -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"); diff --git a/src/args.ts b/src/args.ts index 04fce4f..9e21130 100644 --- a/src/args.ts +++ b/src/args.ts @@ -23,6 +23,7 @@ export type ParsedCLIArgs = { command: string; releaseName?: string; releaseVersion?: string; + releaseDescription?: string; stageName?: string; baseRef?: string; includePaths: string[]; @@ -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" }, @@ -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"] diff --git a/src/index.ts b/src/index.ts index fb84434..7d16ac9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,6 +65,7 @@ Commands: Options: --name= Custom release name --release-version= Release version identifier + --description= Release description (empty string clears it) --stage= Deployment stage (required for update) --include-paths= Filter commits by file paths (comma-separated globs) --include-subjects= Filter commits whose subject (first line) matches the regex @@ -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/**" @@ -123,6 +125,7 @@ const { command, releaseName, releaseVersion, + releaseDescription, stageName, baseRef, includePaths, @@ -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) { @@ -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; } @@ -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)}`); @@ -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; } @@ -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"); @@ -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; } @@ -503,6 +512,7 @@ async function updateCommand(): Promise<{ stage: stageName, version: releaseVersion, name: releaseName, + description: releaseDescription, links, documents, releaseNotes, @@ -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"); @@ -611,6 +621,7 @@ async function syncRelease( releaseLinks: ReleaseLink[], releaseDocuments: ReleaseDocument[], releaseNotesValue: ReleaseNotes | undefined, + description: string | undefined, ): Promise { const currentSha = await getCurrentGitInfo().commit; if (!currentSha) { @@ -643,6 +654,7 @@ async function syncRelease( input: { name: releaseName, version: releaseVersion, + description, commitSha: currentSha, issueReferences, revertedIssueReferences: revertedIssueReferences.length > 0 ? revertedIssueReferences : undefined, @@ -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; @@ -692,6 +705,7 @@ async function completeRelease(options: { links: releaseLinks, documents: releaseDocuments, releaseNotes: notesValue, + description, } = options; const response = await apiRequest( @@ -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, @@ -727,6 +742,7 @@ async function updateReleaseByPipeline(options: { stage?: string; version?: string; name?: string; + description?: string; links: ReleaseLink[]; documents: ReleaseDocument[]; releaseNotes?: ReleaseNotes; @@ -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( ` mutation releaseUpdateByPipelineByAccessKey($input: ReleaseUpdateByPipelineInputBase!) { @@ -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,