From 8bc5f47b1c426bab0196c2dea16a3e4e15bb192c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 12 Oct 2026 20:51:22 +0700 Subject: [PATCH] Add a Report a bug button to plugin detail pages (#6721) Links to a new issue on the plugin's GitHub repository, from the registry's repository URL. Monorepo folder URLs resolve to the repository; non-GitHub sources show no button. Co-authored-by: paseo-bot[bot] --- packages/website/e2e/plugins.spec.ts | 6 ++++++ packages/website/src/plugins/registry.test.ts | 15 +++++++++++++++ packages/website/src/plugins/registry.ts | 13 +++++++++++++ .../src/routes/plugins/$owner_.$slug.tsx | 17 +++++++++++++++-- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/website/e2e/plugins.spec.ts b/packages/website/e2e/plugins.spec.ts index bc6b5ad6d0..6eb9f7d816 100644 --- a/packages/website/e2e/plugins.spec.ts +++ b/packages/website/e2e/plugins.spec.ts @@ -98,6 +98,11 @@ test("browses from the directory into a category, a plugin, and its author", asy "href", "/plugins/category/git", ); + // The source is a folder in a monorepo; issues are filed on the repository. + await expect(page.getByRole("link", { name: "Report a bug" })).toHaveAttribute( + "href", + "https://github.com/omercnet/paseo-plugins/issues/new", + ); await page.getByRole("link", { name: "Omer Cohen" }).click(); await expect(page).toHaveURL(/\/plugins\/omercnet$/); @@ -764,6 +769,7 @@ async function expectPluginOverviewSafe(page: Page) { }); expect(violations).toEqual([]); await expect(page.getByRole("link", { name: "Source", exact: true })).toHaveCount(0); + await expect(page.getByRole("link", { name: "Report a bug" })).toHaveCount(0); await expect(page.getByRole("img", { name: /screenshot/ })).toHaveCount(0); await expect(page.getByRole("heading", { level: 1 })).toHaveText(securityPlugin.name); await expect( diff --git a/packages/website/src/plugins/registry.test.ts b/packages/website/src/plugins/registry.test.ts index cf3af56794..d8f343b266 100644 --- a/packages/website/src/plugins/registry.test.ts +++ b/packages/website/src/plugins/registry.test.ts @@ -11,6 +11,7 @@ import { type Plugin, readmeBody, recentlyUpdated, + reportBugUrl, searchPlugins, updatedAgo, } from "./registry"; @@ -292,6 +293,20 @@ describe("plugin registry", () => { expect(installCommand(plugin)).toBe("paseo plugin add acme/example"); expect(formatInstalls(1250)).toBe("1.3k"); }); + it("reports bugs on the source repository's GitHub issue tracker", () => { + const at = (url: string) => reportBugUrl({ ...plugin, repository: { url } }); + expect(at("https://github.com/acme/example")).toBe( + "https://github.com/acme/example/issues/new", + ); + expect(at("https://github.com/acme/plugins.git")).toBe( + "https://github.com/acme/plugins/issues/new", + ); + expect(at("https://github.com/acme/plugins/tree/HEAD/packages/example")).toBe( + "https://github.com/acme/plugins/issues/new", + ); + expect(at("https://gitlab.com/acme/example")).toBeUndefined(); + expect(at("javascript:alert(1)")).toBeUndefined(); + }); }); /** In-memory stand-in for the website KV namespace, covering the calls the counter makes. */ diff --git a/packages/website/src/plugins/registry.ts b/packages/website/src/plugins/registry.ts index df8dfb4ca6..7d99cb6789 100644 --- a/packages/website/src/plugins/registry.ts +++ b/packages/website/src/plugins/registry.ts @@ -1,4 +1,9 @@ import type { z } from "zod"; +import { + isGitHubHost, + parseGitHubRemoteIdentity, + parseGitRemoteLocation, +} from "@getpaseo/protocol/git-remote"; import { pluginOverviewUrl } from "@getpaseo/protocol/plugin-overview"; import { pluginMediaKind, type PublishedPluginSchema } from "@getpaseo/protocol/plugin-registry"; import { scoreTextFields } from "@getpaseo/protocol/search/text-match"; @@ -53,6 +58,14 @@ export function npmUrl(plugin: Plugin): string | undefined { ? `https://www.npmjs.com/package/${plugin.artifact.package}` : undefined; } +/** A new issue on the plugin's GitHub repository; absent when the source is hosted elsewhere. */ +export function reportBugUrl(plugin: Plugin): string | undefined { + const location = parseGitRemoteLocation(plugin.repository.url); + if (!location || !isGitHubHost(location.host)) return undefined; + // A monorepo source points at the plugin's folder; issues belong to the repository. + const repository = parseGitHubRemoteIdentity(location.path.split("/").slice(0, 2).join("/")); + return repository ? `https://github.com/${repository.repo}/issues/new` : undefined; +} export function authorNpmUrl(author: Author): string | undefined { return author.npm ? `https://www.npmjs.com/~${author.npm}` : undefined; } diff --git a/packages/website/src/routes/plugins/$owner_.$slug.tsx b/packages/website/src/routes/plugins/$owner_.$slug.tsx index 2000067e00..b3d02177aa 100644 --- a/packages/website/src/routes/plugins/$owner_.$slug.tsx +++ b/packages/website/src/routes/plugins/$owner_.$slug.tsx @@ -1,5 +1,6 @@ import { createFileRoute, notFound } from "@tanstack/react-router"; import { ExternalLink } from "lucide-react"; +import { GitHubIcon } from "~/components/brand-icons"; import { CodeBlock } from "~/components/code-block"; import { PluginOverview, PluginContentLink } from "~/plugins/overview"; import { SiteShell } from "~/components/site-shell"; @@ -14,6 +15,7 @@ import { npmUrl, pluginVersion, readmeBody, + reportBugUrl, } from "~/plugins"; import { AuthorAvatar, AuthorLink } from "~/plugins/author-link"; import { InstallCount } from "~/plugins/install-count"; @@ -48,6 +50,8 @@ export const Route = createFileRoute("/plugins/$owner_/$slug")({ const META_LINK_CLASS = "inline-flex items-center gap-1 text-xs text-extra-muted-foreground transition-colors hover:text-muted-foreground"; +const REPORT_BUG_CLASS = + "inline-flex items-center gap-2 rounded-lg border border-white/12 px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"; const AUTHOR_LINK_CLASS = "inline-flex items-center gap-1.5 text-xs text-extra-muted-foreground transition-colors hover:text-muted-foreground"; @@ -56,6 +60,7 @@ function PluginPage() { const category = getCategory(plugin.categories[0]); const author = getAuthor(plugin); const npm = npmUrl(plugin); + const reportBug = reportBugUrl(plugin); return ( @@ -64,8 +69,16 @@ function PluginPage() {

{plugin.name}

{plugin.description}

-
- {installCommand(plugin)} +
+
+ {installCommand(plugin)} +
+ {reportBug && ( + + + Report a bug + + )}