Skip to content
Merged
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.
### Added

- a copy-paste GitHub adoption kit with a maintainer policy and reproducible bug-report Issue Form; and
- a public design-partner pilot and feature-request workflow.
- a public design-partner pilot and feature-request workflow; and
- `bugbundle init --github` for safe, one-command installation of the policy and Issue Form.

## [0.2.0] - 2026-07-17

Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ bugbundle verify --json bugbundle.zip

## Adopt it in your project

The [GitHub adoption kit](examples/github/README.md) contains a maintainer-owned policy and Issue Form that you can copy into a Node.js repository. Review the allowlist and replace the example test command before publishing the form.
Install a maintainer-owned policy and GitHub Issue Form in one command:

```bash
npx bugbundle init --github
```

This creates `.bugbundle.yml` and `.github/ISSUE_TEMPLATE/bug-report.yml`. It refuses to overwrite either file unless you explicitly pass `--force`. Review the allowlist and replace the example `npm test` command before publishing the form. The [GitHub adoption kit](examples/github/README.md) documents the generated files and safety checklist.

We are recruiting five Node.js CLI, build-tool, or library maintainers for a hands-on design-partner pilot. [Introduce your repository in the pilot discussion](https://github.com/bugbundleZ/bugbundle/discussions/9).

Expand Down
9 changes: 5 additions & 4 deletions examples/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

This directory is a copy-paste starting point for maintainers who want reproducible, reviewable bug reports from Node.js projects.

1. Copy `.bugbundle.yml` to the repository root.
1. Run `npx bugbundle init --github` from the repository root. The command refuses to overwrite either generated file unless `--force` is explicit.
2. Review every `files.include` pattern. Add source files only when they are safe and necessary to reproduce failures.
3. Copy `bug-report.yml` to `.github/ISSUE_TEMPLATE/bug-report.yml`.
4. Replace the example `npm test` command with the narrowest stable reproduction command for the project.
5. Run the workflow yourself before asking contributors to use it.
3. Replace the example `npm test` command in `.github/ISSUE_TEMPLATE/bug-report.yml` with the narrowest stable reproduction command for the project.
4. Run the workflow yourself before asking contributors to use it.

You can also copy the files in this directory manually when you want to customize them before installation.

Reporters should run `npx bugbundle@0.2.0 preview`, inspect the complete allowlist, capture the failure, and review the ZIP before attaching it. A BugBundle can still contain sensitive project information that no generic redactor can recognize.

Expand Down
4 changes: 2 additions & 2 deletions examples/github/bug-report.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Reproducible bug report
description: Report a failure with a reviewed BugBundle
title: "[Bug]: "
labels: [bug, needs-triage]
labels: []
body:
- type: markdown
attributes:
Expand Down Expand Up @@ -35,7 +35,7 @@ body:
id: version
attributes:
label: BugBundle version
placeholder: 0.2.0
placeholder: "0.2.0"
validations:
required: true
- type: dropdown
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ npx bugbundle capture --output issue.zip -- npm test
npx bugbundle verify issue.zip
```

Add a `.bugbundle.yml` policy and GitHub Issue Form without overwriting existing files:

```bash
npx bugbundle init --github
```

Verification does not execute code. Replaying a bundle requires the explicit `--run` option. Review files with `bugbundle preview` before sharing an archive.

Use `--json` with `init`, `preview`, `capture`, `inspect`, or `verify` for machine-readable stdout and structured errors on stderr.
19 changes: 18 additions & 1 deletion packages/cli/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { spawn } from "node:child_process";
import { mkdtemp, realpath, writeFile } from "node:fs/promises";
import { mkdtemp, readFile, realpath, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
Expand Down Expand Up @@ -62,6 +62,23 @@ describe("bugbundle CLI", () => {
expect(replay.code).toBe(0);
expect(JSON.parse(replay.stdout).replay).toMatchObject({ exitCode: 6, matched: true });
});

it("installs the GitHub adoption files with stable JSON output", async () => {
const cwd = await mkdtemp(join(tmpdir(), "bugbundle-cli-github-"));
const initialized = await run(["init", "--github", "--json"], cwd);

expect(initialized.code).toBe(0);
const result = JSON.parse(initialized.stdout);
expect(await realpath(result.configPath)).toBe(await realpath(join(cwd, ".bugbundle.yml")));
expect(await realpath(result.githubIssueFormPath)).toBe(
await realpath(join(cwd, ".github", "ISSUE_TEMPLATE", "bug-report.yml")),
);
await expect(readFile(result.githubIssueFormPath, "utf8")).resolves.toContain("npx bugbundle@0.2.0 preview");

const duplicate = await run(["init", "--github", "--json"], cwd);
expect(duplicate.code).toBe(1);
expect(JSON.parse(duplicate.stderr).error).toMatchObject({ code: "RUNTIME_ERROR" });
});
});

async function run(args: readonly string[], cwd = process.cwd()): Promise<{ code: number | null; stdout: string; stderr: string }> {
Expand Down
23 changes: 18 additions & 5 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
#!/usr/bin/env node
import { readFile } from "node:fs/promises";
import {
captureCommand,
inspectBundle,
previewProjectFiles,
verifyBundle,
writeDefaultConfig,
writeGitHubSetup,
} from "@bugbundle/core";

const HELP = `BugBundle captures and verifies local bug report artifacts.

Usage:
bugbundle init [--force] [--json]
bugbundle init [--github] [--force] [--json]
bugbundle preview [--json]
bugbundle capture [--output <file>] [--json] -- <command> [arguments...]
bugbundle inspect <bundle.zip> [--json]
Expand All @@ -37,14 +39,25 @@ async function main(args: readonly string[]): Promise<number> {

async function init(args: readonly string[]): Promise<number> {
const json = args.includes("--json");
const unknown = args.filter((value) => value !== "--force" && value !== "--json");
const unknown = args.filter((value) => value !== "--github" && value !== "--force" && value !== "--json");
if (unknown.length > 0) return usageError(`Unknown init option: ${unknown[0]}`, json);
const path = await writeDefaultConfig(process.cwd(), args.includes("--force"));
if (json) process.stdout.write(`${JSON.stringify({ configPath: path }, null, 2)}\n`);
else process.stdout.write(`${path}\n`);
const force = args.includes("--force");
const result = args.includes("--github")
? await writeGitHubSetup(process.cwd(), { force, cliVersion: await readCliVersion() })
: { configPath: await writeDefaultConfig(process.cwd(), force) };
if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
else process.stdout.write(`${Object.values(result).join("\n")}\n`);
return 0;
}

async function readCliVersion(): Promise<string> {
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
version?: unknown;
};
if (typeof packageJson.version !== "string") throw new Error("BugBundle package version is missing");
return packageJson.version;
}

async function preview(args: readonly string[]): Promise<number> {
const json = args.includes("--json");
const unknown = args.filter((value) => value !== "--json");
Expand Down
56 changes: 56 additions & 0 deletions packages/core/src/github-setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
import { GITHUB_ISSUE_FORM, writeGitHubSetup } from "./github-setup.js";

describe("GitHub setup", () => {
it("writes a config and valid Issue Form without project-specific labels", async () => {
const cwd = await mkdtemp(join(tmpdir(), "bugbundle-github-"));
const result = await writeGitHubSetup(cwd, { cliVersion: "9.8.7" });

expect(result).toEqual({
configPath: join(cwd, ".bugbundle.yml"),
githubIssueFormPath: join(cwd, GITHUB_ISSUE_FORM),
});
const issueForm = parse(await readFile(result.githubIssueFormPath, "utf8"));
expect(issueForm).toMatchObject({
name: "Reproducible bug report",
labels: [],
body: expect.any(Array),
});
await expect(readFile(result.githubIssueFormPath, "utf8")).resolves.toContain("npx bugbundle@9.8.7 preview");
});

it("refuses the whole setup when either target already exists", async () => {
const cwd = await mkdtemp(join(tmpdir(), "bugbundle-github-existing-"));
const issueFormPath = join(cwd, GITHUB_ISSUE_FORM);
await mkdir(join(cwd, ".github", "ISSUE_TEMPLATE"), { recursive: true });
await writeFile(issueFormPath, "maintainer-owned\n");

await expect(writeGitHubSetup(cwd, { cliVersion: "0.2.0" })).rejects.toThrow(
"Refusing to overwrite existing file",
);
await expect(readFile(join(cwd, ".bugbundle.yml"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
await expect(readFile(issueFormPath, "utf8")).resolves.toBe("maintainer-owned\n");
});

it("overwrites both generated files only with force", async () => {
const cwd = await mkdtemp(join(tmpdir(), "bugbundle-github-force-"));
const result = await writeGitHubSetup(cwd, { cliVersion: "0.2.0" });
await writeFile(result.configPath, "custom config\n");
await writeFile(result.githubIssueFormPath, "custom form\n");

await writeGitHubSetup(cwd, { force: true, cliVersion: "0.2.0" });
await expect(readFile(result.configPath, "utf8")).resolves.toContain("schemaVersion: 1");
await expect(readFile(result.githubIssueFormPath, "utf8")).resolves.toContain("Reproducible bug report");
});

it("rejects a version that could inject Issue Form YAML", async () => {
const cwd = await mkdtemp(join(tmpdir(), "bugbundle-github-version-"));
await expect(writeGitHubSetup(cwd, { cliVersion: "1.0.0\nlabels: [unsafe]" })).rejects.toThrow(
"Invalid BugBundle CLI version",
);
});
});
121 changes: 121 additions & 0 deletions packages/core/src/github-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { access, mkdir, unlink, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { CONFIG_FILE, writeDefaultConfig } from "./config.js";

export const GITHUB_ISSUE_FORM = ".github/ISSUE_TEMPLATE/bug-report.yml";

export interface GitHubSetupResult {
readonly configPath: string;
readonly githubIssueFormPath: string;
}

export interface GitHubSetupOptions {
readonly force?: boolean;
readonly cliVersion: string;
}

function issueForm(cliVersion: string): string {
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(cliVersion)) {
throw new Error(`Invalid BugBundle CLI version: ${cliVersion}`);
}
return `name: Reproducible bug report
description: Report a failure with a reviewed BugBundle
title: "[Bug]: "
labels: []
body:
- type: markdown
attributes:
value: |
Thanks for helping us reproduce the problem.

Run these commands from a safe reproduction project. Replace \`npm test\` if the maintainer documents another command.

\`\`\`bash
npx bugbundle@${cliVersion} preview
npx bugbundle@${cliVersion} capture --output bugbundle.zip -- npm test
npx bugbundle@${cliVersion} inspect bugbundle.zip
\`\`\`

Review the complete ZIP before uploading it. Never attach secrets or proprietary source code.
- type: textarea
id: description
attributes:
label: Description
description: What happened, and what did you expect?
validations:
required: true
- type: textarea
id: bundle
attributes:
label: Reviewed BugBundle
description: Drag \`bugbundle.zip\` here after reviewing its complete contents.
validations:
required: true
- type: input
id: version
attributes:
label: BugBundle version
placeholder: "${cliVersion}"
validations:
required: true
- type: dropdown
id: operating-system
attributes:
label: Operating system
options:
- Linux
- macOS
- Windows
- Other
validations:
required: true
- type: checkboxes
id: safety
attributes:
label: Safety confirmation
options:
- label: I ran \`bugbundle preview\` and reviewed every file in the bundle.
required: true
- label: The bundle contains no secrets or proprietary source code.
required: true
`;
}

export async function writeGitHubSetup(
cwd: string,
options: GitHubSetupOptions,
): Promise<GitHubSetupResult> {
const { force = false, cliVersion } = options;
const configPath = join(cwd, CONFIG_FILE);
const githubIssueFormPath = join(cwd, GITHUB_ISSUE_FORM);
const form = issueForm(cliVersion);

if (!force) {
for (const path of [configPath, githubIssueFormPath]) {
if (await exists(path)) throw new Error(`Refusing to overwrite existing file: ${path}`);
}
}

await mkdir(dirname(githubIssueFormPath), { recursive: true });
let createdConfig = false;
try {
await writeDefaultConfig(cwd, force);
createdConfig = true;
await writeFile(githubIssueFormPath, form, { encoding: "utf8", flag: force ? "w" : "wx" });
} catch (error) {
if (createdConfig && !force) await unlink(configPath).catch(() => undefined);
throw error;
}

return { configPath, githubIssueFormPath };
}

async function exists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
throw error;
}
}
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ export type {
VerifyResult,
} from "./types.js";
export { redactText } from "./redact.js";
export { GITHUB_ISSUE_FORM, writeGitHubSetup } from "./github-setup.js";
export type { GitHubSetupOptions, GitHubSetupResult } from "./github-setup.js";