diff --git a/README.md b/README.md index 04c2d33..3b9d4b8 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ The command needs the `issue_comment` trigger and runs from your default branch | `parsing_model` | `google/gemini-3.1-flash-lite-preview` | Parsing model. OpenRouter default shown; other providers use their own engine default. | | `comment_header` | `Architecture review` | Heading for the PR comment. | | `trigger_command` | `/codeboarding` | Slash command for trusted on-demand runs. | -| `cta_base_url` | empty | Click-proxy base URL for the editor/extension links (tracks owner/repo/pr). Empty links straight to the editor deep link and Marketplace. | +| `cta_base_url` | empty | Click-proxy base URL: deep-links the editor link into VS Code/Cursor and adds a "get the extension" link (tracks owner/repo/pr). Empty links to the extension listing instead (GitHub strips `vscode:`/`cursor:` from comments). | ## Outputs diff --git a/action.yml b/action.yml index 5badad0..356e0d4 100644 --- a/action.yml +++ b/action.yml @@ -51,7 +51,7 @@ inputs: required: false default: '1' cta_base_url: - description: 'Base URL of the click proxy (e.g. https://go.codeboarding.org) for the "open in VS Code/Cursor" / "get the extension" links, so owner/repo/pr are tracked. Empty (default) links straight to the editor deep link and Marketplace instead.' + description: 'Base URL of the click proxy (e.g. https://go.codeboarding.org). When set, the editor link deep-links into VS Code/Cursor via the proxy and a "get the extension" link is added (owner/repo/pr tracked). Empty (default) links to the extension listing instead, since GitHub strips vscode:/cursor: schemes from comment links.' required: false default: '' trigger_command: @@ -573,8 +573,8 @@ runs: else echo "$N components changed"; fi } - # CTA footer: editor + extension links (via the click proxy when CTA_BASE is - # set, else straight to the editor deep link / Marketplace) plus the ⚠️ banner. + # CTA footer: an editor link (proxy deep-link when CTA_BASE is set, else the + # extension's https listing — GitHub strips vscode:/cursor:) plus the ⚠️ banner. cta() { python3 "$ACTION_PATH/scripts/build_cta.py" \ --cta-base "$CTA_BASE" --owner "$OWNER" --repo "$REPO" --pr "$PR" \ diff --git a/scripts/build_cta.py b/scripts/build_cta.py index 070d27d..5ee509d 100644 --- a/scripts/build_cta.py +++ b/scripts/build_cta.py @@ -1,13 +1,15 @@ """Build the call-to-action footer appended to the architecture-diff PR comment. -The footer drives straight to the VS Code/Cursor **extension**: an "open this -architecture in your editor" link (editor-specific) plus an "install the -extension" link, and a warning banner when real health findings exist. When a -click proxy (``cta_base``) is set the links route through it so owner/repo/pr are -tracked; otherwise they point at the final destinations directly — the editor -``:extension/...`` deep link and the Marketplace listing. A -no-install hosted-webview ("explore in browser") tier is intentionally deferred -(see docs/COMMIT_STRATEGY.md) — the committed analysis already supports it later. +The footer drives to the VS Code/Cursor **extension** with an editor link, and a +warning banner when real health findings exist. With a click proxy (``cta_base``) +the links route through it (owner/repo/pr tracked) and can deep-link into the +editor (the proxy redirects to a ``vscode:``/``cursor:`` URL) plus a separate +"install the extension" link. Without a proxy GitHub's comment sanitizer strips +custom ``vscode:``/``cursor:`` schemes — a deep link would render as dead text — +so the editor link points at the extension's plain-https listing instead (VS Code +Marketplace, Cursor via Open VSX), which is the only clickable option. A no-install +hosted-webview ("explore in browser") tier is intentionally deferred (see +docs/COMMIT_STRATEGY.md) — the committed analysis already supports it later. Editor coverage is deliberately limited to **VS Code and Cursor**. Per the 2025 Stack Overflow Developer Survey (https://survey.stackoverflow.co/2025/technology/), @@ -47,18 +49,22 @@ def detect_editors(repo_path: Path) -> list[str]: _EDITOR_LABEL = {"vscode": "VS Code", "cursor": "Cursor"} -# No-proxy fallback targets: the final destinations the click proxy would route to. -_EXTENSION_ID = "Codeboarding.codeboarding" -_EDITOR_DEEPLINK = {e: f"{e}:extension/{_EXTENSION_ID}" for e in _EDITOR_LABEL} -_MARKETPLACE_URL = f"https://marketplace.visualstudio.com/items?itemName={_EXTENSION_ID}" +# No-proxy editor targets. Must be plain https: GitHub strips custom URI schemes +# (vscode:/cursor:) from comment links, so a deep link renders as dead text. Each +# editor points at its extension listing instead — clickable, and installs there. +_EDITOR_MARKETPLACE = { + "vscode": "https://marketplace.visualstudio.com/items?itemName=Codeboarding.codeboarding", + "cursor": "https://open-vsx.org/extension/CodeBoarding/codeboarding", +} def build_cta(cta_base: str, owner: str, repo: str, pr: str, repo_path: Path, issues: int = 0) -> str: - """Return the markdown CTA footer: a health-warning banner plus editor/extension links. + """Return the markdown CTA footer: a health-warning banner plus an editor link. - With a ``cta_base`` proxy the links route through it (owner/repo/pr tracked); - without one they point straight to the destinations the proxy would route to — - the editor's ``:extension/...`` deep link and the Marketplace listing. + With a ``cta_base`` proxy the links route through it (owner/repo/pr tracked), + deep-link into the editor, and add a separate "get the extension" link. Without + a proxy the editor link is the extension's https listing (GitHub strips custom + ``vscode:``/``cursor:`` schemes), and the redundant install link is dropped. The ⚠️ banner shows whenever ``issues > 0``. """ parts: list[str] = [] @@ -74,14 +80,15 @@ def link(path: str, **extra: str) -> str: return f"{base}/{path}?" + urlencode({"owner": owner, "repo": repo, "pr": pr, **extra}) editor_href = {e: link("open-in-editor", editor=e) for e in editors} - extension_href = link("use-marketplace") + extension_href: str | None = link("use-marketplace") else: - editor_href = {e: _EDITOR_DEEPLINK[e] for e in editors} - extension_href = _MARKETPLACE_URL + editor_href = {e: _EDITOR_MARKETPLACE[e] for e in editors} + extension_href = None editor_links = " · ".join(f"[**Open in {_EDITOR_LABEL[e]} →**]({editor_href[e]})" for e in editors) parts.append(f"See this architecture in your editor: {editor_links}") - parts.append(f"💡 New to CodeBoarding? [**Get the extension →**]({extension_href})") + if extension_href: + parts.append(f"💡 New to CodeBoarding? [**Get the extension →**]({extension_href})") lines = ["", "---"] for p in parts: diff --git a/tests/test_build_cta.py b/tests/test_build_cta.py index 38ea171..1f37f6f 100644 --- a/tests/test_build_cta.py +++ b/tests/test_build_cta.py @@ -31,17 +31,22 @@ def test_both_vscode_first(self): class TestBuildCta(unittest.TestCase): - def test_no_proxy_falls_back_to_editor_deeplink_and_marketplace(self): + def test_no_proxy_links_editor_to_https_listing_no_get_extension(self): out = bc.build_cta("", "o", "r", "1", repo_with(".cursor"), issues=3) self.assertIn("3 architecture issues found", out) - self.assertIn("[**Open in Cursor →**](cursor:extension/Codeboarding.codeboarding)", out) - self.assertIn("marketplace.visualstudio.com/items?itemName=Codeboarding.codeboarding", out) - self.assertNotIn("open-in-editor", out) # no proxy routing + # Cursor -> Open VSX https listing. A cursor: scheme would be stripped by GitHub. + self.assertIn("[**Open in Cursor →**](https://open-vsx.org/extension/CodeBoarding/codeboarding)", out) + self.assertNotIn("cursor:extension", out) + self.assertNotIn("Get the extension", out) # dropped without a proxy self.assertNotIn("Open in VS Code", out) # cursor-only repo - def test_no_proxy_default_vscode_deeplink_no_banner_at_zero(self): + def test_no_proxy_vscode_marketplace_https_no_banner_at_zero(self): out = bc.build_cta("", "o", "r", "1", repo_with()) # neither dir, no issues - self.assertIn("[**Open in VS Code →**](vscode:extension/Codeboarding.codeboarding)", out) + self.assertIn( + "[**Open in VS Code →**](https://marketplace.visualstudio.com/items?itemName=Codeboarding.codeboarding)", out + ) + self.assertNotIn("vscode:extension", out) # custom scheme stripped by GitHub + self.assertNotIn("Get the extension", out) self.assertNotIn("architecture issue", out) # banner suppressed at 0 issues def test_links_banner_and_cursor_only(self):