Skip to content
Merged
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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Creates a release or adds issues to the current release. This is the default com
linear-release sync

# Specify custom name and version
linear-release sync --name="v1.2.0" --version="1.2.0"
linear-release sync --name="Release 1.2.0" --version="1.2.0"
```

### `complete`
Expand Down Expand Up @@ -131,6 +131,18 @@ linear-release update --stage="in review" --version="1.2.0"
| `--version` | `sync`, `complete`, `update` | Release version identifier. For `sync`, defaults to short commit hash. For `complete` and `update`, if omitted, targets the most recent started release. |
| `--stage` | `update` | Target deployment stage (required for `update`) |
| `--include-paths` | `sync` | Filter commits by changed file paths |
| `--json` | `sync`, `complete`, `update` | Output result as JSON |

### JSON Output

Use `--json` to get structured output for scripting.

```bash
linear-release sync --json
# => {"release":{"id":"...","name":"Release 1.2.0","version":"1.2.0","url":"https://linear.app/..."}}
```

When no release is created (e.g. no commits found), `--json` outputs `{"release":null}`.

### Path Filtering

Expand Down
73 changes: 63 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
PullRequestSource,
RepoInfo,
} from "./types";
import { log } from "./log";
import { log, setStderr } from "./log";
import { pluralize } from "./util";
import { buildUserAgent } from "./user-agent";

Expand Down Expand Up @@ -42,6 +42,7 @@ Options:
--version=<version> Release version identifier
--stage=<stage> Deployment stage (required for update)
--include-paths=<paths> Filter commits by file paths (comma-separated globs)
--json Output result as JSON
-v, --version Show version number
-h, --help Show this help message

Expand Down Expand Up @@ -88,6 +89,12 @@ const releaseVersion: string | null = versionArg ? versionArg.replace("--version
const stageArg = process.argv.find((arg) => arg.startsWith("--stage="));
const stageName: string | null = stageArg ? stageArg.replace("--stage=", "").trim() : null;

// Parse --json flag for structured JSON output
const jsonOutput = process.argv.includes("--json");
if (jsonOutput) {
setStderr(true);
}

const logEnvironmentSummary = () => {
log("Using access key authentication");

Expand Down Expand Up @@ -212,7 +219,9 @@ function scanCommits(
};
}

async function syncCommand() {
async function syncCommand(): Promise<{
release: { id: string; name: string; version?: string; url?: string };
} | null> {
logEnvironmentSummary();

// Fetch pipeline settings from API
Expand Down Expand Up @@ -279,7 +288,7 @@ async function syncCommand() {
? `matching ${JSON.stringify(effectiveIncludePaths)}`
: "in the computed range";
log(`No commits found ${reason}. Skipping release creation.`);
return;
return null;
}

const { issueIdentifiers, prNumbers, debugSink } = scanCommits(commits, effectiveIncludePaths);
Expand All @@ -302,9 +311,13 @@ async function syncCommand() {
);

log("Finished");

return { release: { id: release.id, name: release.name, version: release.version, url: release.url } };
}

async function completeCommand() {
async function completeCommand(): Promise<{
release: { id: string; name: string; version?: string; url?: string };
} | null> {
logEnvironmentSummary();

const currentCommit = await getCurrentGitInfo();
Expand All @@ -321,9 +334,22 @@ async function completeCommand() {
}

log("Finished");

return result.release
? {
release: {
id: result.release.id,
name: result.release.name,
version: result.release.version,
url: result.release.url,
},
}
: null;
}

async function updateCommand() {
async function updateCommand(): Promise<{
release: { id: string; name: string; version?: string; url?: string };
} | null> {
logEnvironmentSummary();

if (!stageName) {
Expand All @@ -348,6 +374,17 @@ async function updateCommand() {
}

log("Finished");

return result.release
? {
release: {
id: result.release.id,
name: result.release.name,
version: result.release.version,
url: result.release.url,
},
}
: null;
}

async function getLatestRelease(): Promise<Release | null> {
Expand Down Expand Up @@ -428,6 +465,7 @@ async function syncRelease(
release {
id
name
url
version
commitSha
createdAt
Expand Down Expand Up @@ -461,7 +499,7 @@ async function syncRelease(
async function completeRelease(options: {
version?: string | null;
commitSha?: string | null;
}): Promise<{ success: boolean; release: { name: string } | null }> {
}): Promise<{ success: boolean; release: { id: string; name: string; version?: string; url?: string } | null }> {
const { version, commitSha } = options;

const response = (await linearClient.client.rawRequest(
Expand All @@ -470,7 +508,10 @@ async function completeRelease(options: {
releaseCompleteByAccessKey(input: $input) {
success
release {
id
name
version
url
}
}
}
Expand All @@ -488,7 +529,7 @@ async function completeRelease(options: {

async function updateReleaseByPipeline(options: { stage?: string; version?: string | null }): Promise<{
success: boolean;
release: { name: string; stageName: string } | null;
release: { id: string; name: string; version?: string; url?: string; stageName: string } | null;
}> {
const { stage, version } = options;
const versionInput = version ? `, version: "${version}"` : "";
Expand All @@ -504,7 +545,10 @@ async function updateReleaseByPipeline(options: { stage?: string; version?: stri
releaseUpdateByPipelineByAccessKey(input: { ${inputParts} }) {
success
release {
id
name
version
url
stage {
name
}
Expand All @@ -519,29 +563,38 @@ async function updateReleaseByPipeline(options: { stage?: string; version?: stri
success: result.success,
release: result.release
? {
id: result.release.id,
name: result.release.name,
version: result.release.version,
url: result.release.url,
stageName: result.release.stage?.name ?? "(unknown)",
}
: null,
};
}

async function main() {
let result: { release: { id: string; name: string; version?: string; url?: string } } | null = null;

switch (command) {
case "sync":
await syncCommand();
result = await syncCommand();
break;
case "complete":
await completeCommand();
result = await completeCommand();
break;
case "update":
await updateCommand();
result = await updateCommand();
break;
default:
console.error(`Unknown command: ${command}`);
console.error("Available commands: sync, complete, update");
process.exit(1);
}

if (jsonOutput) {
console.log(JSON.stringify(result ?? { release: null }));
}
}

main().catch((error) => {
Expand Down
12 changes: 11 additions & 1 deletion src/log.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
let useStderr = false;

export function setStderr(value: boolean) {
useStderr = value;
}

export function log(message: string) {
if (process.env.NODE_ENV !== "test") {
console.log(`=> ${message}`);
if (useStderr) {
process.stderr.write(`=> ${message}\n`);
} else {
console.log(`=> ${message}`);
}
}
}
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ export type AccessKeyCompleteReleaseResponse = {
releaseCompleteByAccessKey: {
success: boolean;
release: {
id: string;
name: string;
version?: string;
url?: string;
} | null;
};
};
Expand All @@ -48,7 +51,10 @@ export type AccessKeyUpdateByPipelineResponse = {
releaseUpdateByPipelineByAccessKey: {
success: boolean;
release: {
id: string;
name: string;
version?: string;
url?: string;
stage: {
name: string;
} | null;
Expand Down