Skip to content
Open
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
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,15 @@ linear-release update --stage="in review" --description="Highlights and rollout

### Environment Variables

| Variable | Required | Description |
| --------------------- | -------- | ---------------------------------------------------------------- |
| `LINEAR_ACCESS_KEY` | Yes | Pipeline access key from Linear |
| `LINEAR_VCS_PROVIDER` | No | Override VCS provider detection: `github`, `gitlab`, `bitbucket` |
| Variable | Required | Description |
| --------------------- | -------- | -------------------------------------------------------------------------- |
| `LINEAR_ACCESS_KEY` | Yes | Pipeline access key from Linear |
| `LINEAR_VCS_PROVIDER` | No | Override VCS provider detection: `github`, `gitlab`, `bitbucket`, `origin` |

The provider is detected from the remote hostname, or on GitLab CI from the job environment. Set `LINEAR_VCS_PROVIDER` when neither identifies the host (e.g. self-hosted GitHub Enterprise on a custom domain). When the provider can't be determined, `sync` exits with code `2` before syncing anything.

Cursor Origin remotes (`origin.cursor.com`) use the `origin` provider. Release commit links point to `cursor.com/codebase`. PR references resolve against PRs already imported by Linear's Origin integration; direct issue identifiers work without a PR import. For an Origin mirror whose PRs are hosted on GitHub, set `LINEAR_VCS_PROVIDER=github`.

### CLI Options

| Option | Commands | Description |
Expand Down Expand Up @@ -332,7 +334,7 @@ linear-release sync --document "Deploy log=Deployed to production at $(date -u +
1. **Fetches the latest release** from your Linear pipeline to determine the commit range
2. **Scans commits** between the commit from the last release and the current commit
3. **Extracts issue identifiers** from branch names and commit messages (e.g., `feat/ENG-123-add-feature`)
4. **Detects pull/merge request numbers** from commit messages — GitHub `Title (#42)` / `Merge pull request #42`, and GitLab `See merge request <group>/<project>!42` trailers (emitted whenever a merge commit is created)
4. **Detects pull/merge request numbers** from commit messages — GitHub and Origin `Title (#42)` / `Merge pull request #42`, and GitLab `See merge request <group>/<project>!42` trailers (emitted whenever a merge commit is created)
5. **Syncs data to Linear** that adds issues and provided links to a newly created completed release (continuous pipelines) or the currently in-progress release (scheduled pipelines). PR/MR numbers are sent alongside the repository info, and Linear resolves them back to any issues linked to those PRs/MRs — so issues attached only via a PR/MR (not mentioned in a commit message or branch name) are still picked up.

> [!NOTE]
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Options:

Environment:
LINEAR_ACCESS_KEY Pipeline access key (required)
LINEAR_VCS_PROVIDER Override VCS provider detection: github|gitlab|bitbucket
LINEAR_VCS_PROVIDER Override VCS provider detection: github|gitlab|bitbucket|origin

Examples:
linear-release sync
Expand Down
34 changes: 34 additions & 0 deletions src/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,40 @@ describe("resolveRepoInfo", () => {
expect(resolveRepoInfo(detected, { LINEAR_VCS_PROVIDER: "GitLab" })?.provider).toBe("gitlab");
});

it.each([
"https://origin.cursor.com/acme/repo.git",
"https://token@origin.cursor.com/acme/repo.git",
"git@origin.cursor.com:acme/repo.git",
"https://ORIGIN.CURSOR.COM:443/acme/repo.git",
])("resolves Origin coordinates and web URL from %s", (url) => {
expect(resolveRepoInfo(parseRepoUrl(url), {})).toMatchObject({
provider: "origin",
owner: "acme",
name: "repo",
url: "https://cursor.com/codebase/acme/repo",
});
});

it.each(["https://origin.cursor.com.example.org/acme/repo.git", "https://cursor.com/acme/repo.git"])(
"does not infer Origin from %s",
(url) => {
expect(captureError(() => resolveRepoInfo(parseRepoUrl(url), {})).code).toBe("unknown-provider");
},
);

it("accepts an Origin override", () => {
expect(
resolveRepoInfo(parseRepoUrl("https://git.example.com/acme/repo.git"), { LINEAR_VCS_PROVIDER: "Origin" }),
).toMatchObject({ provider: "origin", url: "https://cursor.com/codebase/acme/repo" });
});

it("allows mirrored repositories to keep using their GitHub provider override", () => {
expect(
resolveRepoInfo(parseRepoUrl("https://origin.cursor.com/acme/repo.git"), { LINEAR_VCS_PROVIDER: "github" })
?.provider,
).toBe("github");
});

it("treats an empty override as unset", () => {
const detected = parseRepoUrl("https://github.com/acme/repo.git");
expect(resolveRepoInfo(detected, { LINEAR_VCS_PROVIDER: "" })?.provider).toBe("github");
Expand Down
22 changes: 18 additions & 4 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ function normalizeHost(host: string): string {
}

function hostToProvider(host: string): RepositoryProvider | null {
if (host === "origin.cursor.com") {
return "origin";
}
if (host === "gitlab.com" || host.includes("gitlab")) {
return "gitlab";
}
Expand All @@ -71,7 +74,9 @@ function hostToProvider(host: string): RepositoryProvider | null {

function parseProvider(value: string): RepositoryProvider | null {
const provider = value.toLowerCase();
return provider === "github" || provider === "gitlab" || provider === "bitbucket" ? provider : null;
return provider === "github" || provider === "gitlab" || provider === "bitbucket" || provider === "origin"
? provider
: null;
}

function inferProviderFromCI(
Expand All @@ -98,7 +103,7 @@ function resolveProvider(repoInfo: RepoInfo, env: Record<string, string | undefi
const provider = parseProvider(override);
if (!provider) {
throw new ConfigurationError(
`Invalid LINEAR_VCS_PROVIDER value "${override}". Expected github, gitlab, or bitbucket.`,
`Invalid LINEAR_VCS_PROVIDER value "${override}". Expected github, gitlab, bitbucket, or origin.`,
"invalid-provider-override",
);
}
Expand All @@ -108,7 +113,7 @@ function resolveProvider(repoInfo: RepoInfo, env: Record<string, string | undefi
const provider = hostToProvider(host) ?? inferProviderFromCI(env, repoInfo, host);
if (!provider) {
throw new ConfigurationError(
`Could not determine the VCS provider for remote host "${host}".\nSet LINEAR_VCS_PROVIDER=github|gitlab|bitbucket in your CI environment.`,
`Could not determine the VCS provider for remote host "${host}".\nSet LINEAR_VCS_PROVIDER=github|gitlab|bitbucket|origin in your CI environment.`,
"unknown-provider",
);
}
Expand All @@ -119,5 +124,14 @@ export function resolveRepoInfo(
repoInfo: RepoInfo | null,
env: Record<string, string | undefined> = process.env,
): ResolvedRepoInfo | null {
return repoInfo ? { ...repoInfo, provider: resolveProvider(repoInfo, env) } : null;
if (!repoInfo) {
return null;
}
const provider = resolveProvider(repoInfo, env);
// Origin's git transport host differs from its web UI, where release commit links point.
const url =
provider === "origin" && repoInfo.owner && repoInfo.name
? `https://cursor.com/codebase/${repoInfo.owner}/${repoInfo.name}`
: repoInfo.url;
return { ...repoInfo, provider, url };
}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export type GitInfo = {
message: string | null;
};

export type RepositoryProvider = "github" | "gitlab" | "bitbucket";
export type RepositoryProvider = "github" | "gitlab" | "bitbucket" | "origin";

export type RepoInfo = {
owner: string | null;
Expand Down
Loading