diff --git a/README.md b/README.md index 81408bc..23c2c9d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Open [http://localhost:3000](http://localhost:3000). | `/` | Marketing landing page | | `/open` | URL scheme receiver — accepts `?owner`, `?repo`, `?issue` params and triggers `worktree://open?...` | +The `/open` route is the public URL API consumed by the GitHub Action, CLI, and integrations. See [docs/api-versioning.md](docs/api-versioning.md) for the versioning policy and parameter reference. + ## License MIT diff --git a/docs/api-versioning.md b/docs/api-versioning.md new file mode 100644 index 0000000..1a0a687 --- /dev/null +++ b/docs/api-versioning.md @@ -0,0 +1,47 @@ +# API Versioning Policy + +## Overview + +The primary "API surface" of this site is the `/open` URL scheme bridge. External consumers (GitHub Action comments, CI scripts, integrations) construct `/open?...` URLs. Breaking changes to accepted parameters must not invalidate old links. + +## Current version: v1 (unversioned) + +All existing `/open` URLs are treated as **v1**. The `v` query parameter is optional and defaults to `1`. + +### v1 parameter sets + +**GitHub issue** +``` +/open?owner=&repo=&issue= +``` + +**GitLab issue** +``` +/open?owner=&repo=&gitlab_issue= +``` + +**Jira issue** +``` +/open?owner=&repo=&jira_issue_key=&jira_host= +``` + +## Versioning convention + +| Version | Path | Status | +| ------- | ---------------- | ------- | +| v1 | `/open` | Current | +| v2+ | `/open?v=2&...` | Future | + +### Rules + +1. **Never remove or rename params from an existing version.** Old links must keep working. +2. **Introduce a new version for breaking changes.** Add `v=2` (or higher) when dropping, renaming, or reinterpreting existing params. +3. **New optional params are non-breaking.** They may be added to an existing version without a version bump. +4. **The `v` param must be a positive integer string.** Unknown values return an error state with a clear message. + +## Adding a new version + +1. Extend the `resolveParams` function in `src/app/open/page.tsx` with a branch for the new version. +2. Add the new param set to this document under a new `## v parameter sets` section. +3. Keep the old version branch intact — do not delete it. +4. Update the version table above. diff --git a/src/app/open/page.tsx b/src/app/open/page.tsx index 9dd3d01..4011a14 100644 --- a/src/app/open/page.tsx +++ b/src/app/open/page.tsx @@ -3,16 +3,18 @@ import { useEffect, useRef, useState } from "react"; import { type IssueParams, buildWorktreeUrl } from "./issue-card"; import { NoParamsView } from "./no-params-view"; +import { UnknownVersionView } from "./unknown-version-view"; import { OpeningView } from "./opening-view"; import { InstallView } from "./install-view"; import { OpenNav, OpenFooter } from "./open-layout"; type Phase = - | "loading" // reading URL params - | "opening" // scheme triggered, awaiting user confirmation - | "success" // user confirmed it opened - | "install" // user says it didn't open → show install guide - | "no-params"; // no valid params in URL + | "loading" // reading URL params + | "opening" // scheme triggered, awaiting user confirmation + | "success" // user confirmed it opened + | "install" // user says it didn't open → show install guide + | "no-params" // no valid params in URL + | "unknown-version"; // unrecognized v= param function get(sp: URLSearchParams, key: string): string { const val = sp.get(key); @@ -42,13 +44,23 @@ function resolveParams(sp: URLSearchParams): IssueParams | null { return { kind: "github", owner, repo, issue }; } +const SUPPORTED_VERSIONS: string[] = ["1"]; + export default function OpenPage() { const [phase, setPhase] = useState("loading"); const [params, setParams] = useState(null); + const [urlVersion, setUrlVersion] = useState("1"); const schemeTriggered = useRef(false); useEffect(() => { const sp = new URLSearchParams(window.location.search); + const raw = sp.get("v"); + const v = raw !== null ? raw : "1"; + setUrlVersion(v); + if (!SUPPORTED_VERSIONS.includes(v)) { + setPhase("unknown-version"); + return; + } const p = resolveParams(sp); if (!p) { setPhase("no-params"); return; } setParams(p); @@ -71,6 +83,7 @@ export default function OpenPage() {
{phase === "no-params" && } + {phase === "unknown-version" && } {phase === "loading" && (
diff --git a/src/app/open/unknown-version-view.tsx b/src/app/open/unknown-version-view.tsx new file mode 100644 index 0000000..92d2e17 --- /dev/null +++ b/src/app/open/unknown-version-view.tsx @@ -0,0 +1,23 @@ +import Link from "next/link"; +import { AlertTriangle } from "lucide-react"; + +export function UnknownVersionView({ version }: { version: string }) { + return ( +
+
+ +
+

Unsupported link version

+

+ This link uses{" "} + v={version}, which is not + supported by this version of the site. The link may have been + generated by a newer version of the Worktree runner. Please update + your runner to open this workspace. +

+ + ← Back to home + +
+ ); +}