mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
feat(cli): file a GitHub issue with a published repro from feedback (#1816)
Add an opt-in --file-issue flag to hyperframes feedback. When set, after sending the usual feedback the CLI publishes a minimal repro of the project to a public URL (consent-gated, mirroring publish --yes) and opens a pre-filled GitHub bug issue draft containing the rating, comment, public repro link, and environment summary. The user reviews and submits the issue under their own account; there is no token, backend, or gh invocation. New --dir selects the project to publish; --yes skips the consent prompt for scripts. URL/body building is extracted into pure, unit-tested helpers.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildIssueUrl, HYPERFRAMES_REPO_URL } from "./feedbackIssue.js";
|
||||
|
||||
function decoded(url: string, key: "title" | "body"): string {
|
||||
const value = new URL(url).searchParams.get(key);
|
||||
return value ?? "";
|
||||
}
|
||||
|
||||
describe("buildIssueUrl", () => {
|
||||
const base = {
|
||||
repoUrl: HYPERFRAMES_REPO_URL,
|
||||
rating: 2,
|
||||
comment: "GSAP timeline froze on seek",
|
||||
repoPublicUrl: "https://hyperframes.dev/p/abc123",
|
||||
environment: "os=darwin/arm64 node=v22.11.0 ffmpeg=yes",
|
||||
cliVersion: "1.2.3",
|
||||
};
|
||||
|
||||
it("points at the repo /issues/new with the bug label", () => {
|
||||
const url = buildIssueUrl(base);
|
||||
expect(url.startsWith(`${HYPERFRAMES_REPO_URL}/issues/new?`)).toBe(true);
|
||||
expect(new URL(url).searchParams.get("labels")).toBe("bug");
|
||||
});
|
||||
|
||||
it("encodes the title and includes rating + repro URL in the body", () => {
|
||||
const url = buildIssueUrl(base);
|
||||
expect(decoded(url, "title")).toBe("[feedback] GSAP timeline froze on seek");
|
||||
const body = decoded(url, "body");
|
||||
expect(body).toContain("2/5");
|
||||
expect(body).toContain("https://hyperframes.dev/p/abc123");
|
||||
expect(body).toContain("os=darwin/arm64");
|
||||
expect(body).toContain("cli=1.2.3");
|
||||
});
|
||||
|
||||
it("falls back to a generic title when there is no comment", () => {
|
||||
const url = buildIssueUrl({ ...base, comment: undefined });
|
||||
expect(decoded(url, "title")).toBe("Render feedback (rating 2/5)");
|
||||
});
|
||||
|
||||
it("truncates an overlong comment in the body", () => {
|
||||
const longComment = "x".repeat(9000);
|
||||
const url = buildIssueUrl({ ...base, comment: longComment });
|
||||
const body = decoded(url, "body");
|
||||
expect(body).not.toContain("x".repeat(9000));
|
||||
expect(body).toContain("…");
|
||||
// Whole URL stays well under the ~8 KB pre-fill limit.
|
||||
expect(url.length).toBeLessThan(8000);
|
||||
});
|
||||
|
||||
it("strips a trailing .git from the repo url", () => {
|
||||
const url = buildIssueUrl({
|
||||
...base,
|
||||
repoUrl: "https://github.com/heygen-com/hyperframes.git",
|
||||
});
|
||||
expect(url.startsWith(`${HYPERFRAMES_REPO_URL}/issues/new?`)).toBe(true);
|
||||
});
|
||||
|
||||
it("notes when no repro link is available", () => {
|
||||
const url = buildIssueUrl({ ...base, repoPublicUrl: undefined });
|
||||
expect(decoded(url, "body")).toContain("Publishing the repro failed");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
// Reading package.json at runtime from the single-file bundled CLI is awkward,
|
||||
// so we keep the canonical repo as a constant. It must match the `repository.url`
|
||||
// in packages/cli/package.json.
|
||||
export const HYPERFRAMES_REPO_URL = "https://github.com/heygen-com/hyperframes";
|
||||
|
||||
const TITLE_MAX = 80;
|
||||
// Pre-filled issue URLs have a practical length limit (~8 KB), so cap the
|
||||
// comment that goes into the body.
|
||||
const COMMENT_MAX = 4000;
|
||||
|
||||
export interface IssueInput {
|
||||
repoUrl: string;
|
||||
rating: number;
|
||||
comment?: string;
|
||||
/** Public URL of the published minimal repro, if publishing succeeded. */
|
||||
repoPublicUrl?: string;
|
||||
/** Doctor summary string (os/node/ffmpeg...). */
|
||||
environment: string;
|
||||
cliVersion: string;
|
||||
}
|
||||
|
||||
function normalizeRepoUrl(repoUrl: string): string {
|
||||
const trimmed = repoUrl
|
||||
.trim()
|
||||
.replace(/\/$/, "")
|
||||
.replace(/\.git$/, "");
|
||||
return trimmed || HYPERFRAMES_REPO_URL;
|
||||
}
|
||||
|
||||
function truncate(value: string, max: number): string {
|
||||
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
||||
}
|
||||
|
||||
function buildIssueTitle(rating: number, comment?: string): string {
|
||||
const firstLine = comment?.split("\n")[0]?.trim();
|
||||
if (!firstLine) return `Render feedback (rating ${rating}/5)`;
|
||||
return `[feedback] ${truncate(firstLine, TITLE_MAX)}`;
|
||||
}
|
||||
|
||||
function buildIssueBody(input: IssueInput): string {
|
||||
const comment = input.comment?.trim();
|
||||
const repro = input.repoPublicUrl
|
||||
? `Published minimal repro: ${input.repoPublicUrl}`
|
||||
: "_Publishing the repro failed, no public link available._";
|
||||
|
||||
return [
|
||||
`**Rating:** ${input.rating}/5`,
|
||||
"",
|
||||
"## Comment",
|
||||
comment ? truncate(comment, COMMENT_MAX) : "_No comment provided._",
|
||||
"",
|
||||
"## Minimal repro",
|
||||
repro,
|
||||
"",
|
||||
"## Environment",
|
||||
"```",
|
||||
input.environment || "(unavailable)",
|
||||
`cli=${input.cliVersion}`,
|
||||
"```",
|
||||
"",
|
||||
"---",
|
||||
"_Filed via `hyperframes feedback --file-issue`._",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildIssueUrl(input: IssueInput): string {
|
||||
const repo = normalizeRepoUrl(input.repoUrl);
|
||||
const title = encodeURIComponent(buildIssueTitle(input.rating, input.comment));
|
||||
const body = encodeURIComponent(buildIssueBody(input));
|
||||
return `${repo}/issues/new?title=${title}&body=${body}&labels=bug`;
|
||||
}
|
||||
Reference in New Issue
Block a user