diff --git a/.craft.yml b/.craft.yml index a9d077321..f1f8f3f38 100644 --- a/.craft.yml +++ b/.craft.yml @@ -1,4 +1,4 @@ -minVersion: "3.7.0" +minVersion: "3.8.0" workspaces: packages/cli: changelog: @@ -16,7 +16,19 @@ workspaces: style: Keep it concise and friendly for developers; avoid hype. versioning: policy: auto - preReleaseCommand: bash -c 'cd packages/cli && node --experimental-strip-types script/bump-version.ts --pre' + # The npm targets bump only packages/cli/package.json. These files ship + # with every CLI release and carry its version too. Craft writes them for + # the release and again for the next -dev version (postReleaseBump). + files: + # Published at the CLI's version; its bundle script fails on a mismatch. + - path: packages/mcp-server/package.json + # The plugin's version stays a plain release version, even on -dev builds. + - path: packages/cli/plugins/sentry-cli/.claude-plugin/plugin.json + stripPrerelease: true + # Skill frontmatter. The skill generator embeds the version too, but it + # needs a gitignored API schema that Craft's checkout doesn't have. + - path: packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md + - path: packages/cli/plugins/sentry-cli/skills/sentry-cli/references/*.md postReleaseBump: true releaseBranchPrefix: release/cli artifactProvider: diff --git a/docs/releases/stdio.md b/docs/releases/stdio.md index e744172e0..111f12f3d 100644 --- a/docs/releases/stdio.md +++ b/docs/releases/stdio.md @@ -66,9 +66,10 @@ smoke-tests the result. The `packages/cli` Craft workspace releases both packages: run the **Release** workflow with `product: cli`. -- `packages/cli/script/bump-version.ts` sets `packages/mcp-server/package.json` - to the CLI's version, both for the release and for the next `-dev` version, - which Craft's `postReleaseBump` writes and pushes after publishing. +- Craft sets `packages/mcp-server/package.json` to the CLI's version through + `versioning.files` in `.craft.yml` (Craft 3.8.0+), both for the release and + for the next `-dev` version, which `postReleaseBump` writes and pushes after + publishing. The bundle script fails if the two versions differ. - `.github/workflows/cli-build.yml` bundles, tests, and packs the package next to the CLI's npm package and uploads it as `mcp-server-npm-package`. diff --git a/packages/cli/script/bump-version.ts b/packages/cli/script/bump-version.ts deleted file mode 100644 index b44f9d021..000000000 --- a/packages/cli/script/bump-version.ts +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env node -/** - * Version Bump Script - * - * Craft's `preReleaseCommand` for the CLI workspace. It runs for the release - * version in `craft prepare`, and again for the next `-dev` version after - * `craft publish` (`postReleaseBump` in .craft.yml), which Craft commits and - * pushes. `@sentry/mcp-server` ships with every CLI release, so it takes the - * same version. - * - * Usage: - * CRAFT_NEW_VERSION=1.0.0 node --experimental-strip-types script/bump-version.ts --pre - * - * Sets CRAFT_NEW_VERSION as: - * - the CLI and @sentry/mcp-server package.json versions - * - the plugin.json version, without the prerelease suffix - * - the tracked CLI skill frontmatter versions - */ - -import { execSync } from "node:child_process"; -import { readdirSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - -const PLUGIN_PATH = "plugins/sentry-cli/.claude-plugin/plugin.json"; -const MCP_SERVER_PATH = "../mcp-server"; -const SKILL_PATH = "plugins/sentry-cli/skills/sentry-cli"; -const SKILL_VERSION_LINE = /^version: [^\r\n]+$/gm; - -/** Regex to match prerelease suffix (e.g., -dev.0, -alpha.1) */ -const PRERELEASE_SUFFIX_REGEX = /-.*$/; - -type PluginJson = { - version: string; - [key: string]: unknown; -}; - -/** - * Read and parse plugin.json - */ -function readPluginJson(): PluginJson { - return JSON.parse(readFileSync(PLUGIN_PATH, "utf-8")) as PluginJson; -} - -/** - * Write plugin.json with consistent formatting - */ -function writePluginJson(plugin: PluginJson): void { - writeFileSync(PLUGIN_PATH, `${JSON.stringify(plugin, null, 2)}\n`); -} - -/** - * Strip prerelease suffix from version string. - * "1.0.0-dev.0" → "1.0.0" - * "1.0.0" → "1.0.0" - */ -function stripPrerelease(version: string): string { - return version.replace(PRERELEASE_SUFFIX_REGEX, ""); -} - -/** Update the tracked skill frontmatter without requiring a network-backed schema build. */ -function syncSkillVersions(version: string): void { - const references = readdirSync(join(SKILL_PATH, "references")) - .filter((name) => name.endsWith(".md")) - .map((name) => join(SKILL_PATH, "references", name)); - if (references.length === 0) { - throw new Error("Generated CLI skill references are missing"); - } - - const files = [join(SKILL_PATH, "SKILL.md"), ...references].map((path) => { - const content = readFileSync(path, "utf-8"); - const end = content.indexOf("\n---\n", 4); - if (!content.startsWith("---\n") || end === -1) { - throw new Error(`Invalid CLI skill frontmatter: ${path}`); - } - const frontmatter = content.slice(0, end); - const versions = frontmatter.match(SKILL_VERSION_LINE); - if (versions?.length !== 1) { - throw new Error(`Expected one CLI skill version: ${path}`); - } - return { - path, - content: - frontmatter.replace(SKILL_VERSION_LINE, `version: ${version}`) + - content.slice(end), - }; - }); - - for (const { path, content } of files) { - writeFileSync(path, content); - } -} - -/** - * Execute a shell command with output inherited to stdout/stderr - */ -function exec(command: string, cwd?: string): void { - execSync(command, { stdio: "inherit", cwd }); -} - -/** Give `@sentry/mcp-server` the CLI's version. */ -function setMcpServerVersion(version: string): void { - exec( - `npm --no-git-tag-version --allow-same-version version ${version}`, - MCP_SERVER_PATH, - ); - console.log(`Updated @sentry/mcp-server to ${version}`); -} - -/** - * Pre-release: Set version from CRAFT_NEW_VERSION environment variable - */ -function preRelease(): void { - const version = process.env.CRAFT_NEW_VERSION; - if (!version) { - console.error("Error: CRAFT_NEW_VERSION environment variable is required"); - process.exit(1); - } - - console.log(`Setting version to ${version}`); - - // Update package.json via npm (handles formatting consistently) - exec(`npm --no-git-tag-version version ${version}`); - setMcpServerVersion(version); - - // Update plugin.json (strip prerelease suffix for cleaner version) - const plugin = readPluginJson(); - plugin.version = stripPrerelease(version); - writePluginJson(plugin); - - console.log(`Updated plugin.json to ${plugin.version}`); - - // The generator embeds package.json's version in every tracked skill file. - // Sync only its frontmatter here: the full generator needs an API schema that - // is gitignored and may not exist in Craft's checkout. - syncSkillVersions(version); -} - -if (process.argv.includes("--pre")) { - preRelease(); -} else { - console.log(` -Usage: CRAFT_NEW_VERSION= node --experimental-strip-types script/bump-version.ts --pre -`); - process.exit(1); -} diff --git a/packages/cli/test/script/bump-version.test.ts b/packages/cli/test/script/bump-version.test.ts deleted file mode 100644 index 14ceb48cd..000000000 --- a/packages/cli/test/script/bump-version.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { - chmodSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { afterEach, expect, test } from "vitest"; - -const script = resolve( - dirname(fileURLToPath(import.meta.url)), - "../../script/bump-version.ts", -); -const skill = "plugins/sentry-cli/skills/sentry-cli"; -const fixtures: string[] = []; - -afterEach(() => { - for (const fixture of fixtures.splice(0)) - rmSync(fixture, { recursive: true, force: true }); -}); - -/** - * Lay out `cli/` and its sibling `mcp-server/` like `packages/`, with a fake - * `npm` binary whose `npm version` writes the requested version. - */ -function createFixture(): { root: string; cli: string; bin: string } { - const root = mkdtempSync(join(tmpdir(), "cli-bump-version-")); - fixtures.push(root); - const cli = join(root, "cli"); - const bin = join(root, "bin"); - mkdirSync(bin); - mkdirSync(join(cli, skill, "references"), { recursive: true }); - mkdirSync(join(cli, "plugins/sentry-cli/.claude-plugin"), { - recursive: true, - }); - mkdirSync(join(root, "mcp-server")); - writeFileSync(join(cli, "package.json"), '{"version":"0.47.0"}\n'); - writeFileSync( - join(root, "mcp-server/package.json"), - '{"version":"0.42.0"}\n', - ); - writeFileSync( - join(cli, "plugins/sentry-cli/.claude-plugin/plugin.json"), - '{"version":"0.47.0"}\n', - ); - for (const path of ["SKILL.md", "references/auth.md"]) { - writeFileSync( - join(cli, skill, path), - "---\nname: sentry-cli\nversion: 0.47.0-dev.0\n---\n\nContent\n", - ); - } - - writeFileSync( - join(bin, "npm"), - `#!/usr/bin/env node -require("node:fs").writeFileSync("package.json", JSON.stringify({ version: process.argv.at(-1) }) + "\\n"); -`, - ); - chmodSync(join(bin, "npm"), 0o755); - return { root, cli, bin }; -} - -function runScript( - { cli, bin }: { cli: string; bin: string }, - env: Record, -) { - return spawnSync( - process.execPath, - ["--experimental-strip-types", script, "--pre"], - { - cwd: cli, - encoding: "utf8", - env: { - HOME: cli, - PATH: `${bin}:${process.env.PATH ?? ""}`, - XDG_CONFIG_HOME: cli, - ...env, - }, - timeout: 15_000, - }, - ); -} - -function readVersion(path: string): string { - return (JSON.parse(readFileSync(path, "utf8")) as { version: string }) - .version; -} - -test("pre-release bump gives @sentry/mcp-server the release version", () => { - const fixture = createFixture(); - - const result = runScript(fixture, { CRAFT_NEW_VERSION: "0.48.0" }); - - expect(result.status, result.stderr).toBe(0); - expect(readVersion(join(fixture.cli, "package.json"))).toBe("0.48.0"); - expect(readVersion(join(fixture.root, "mcp-server/package.json"))).toBe( - "0.48.0", - ); -}); - -// Craft's post-release bump reruns the pre-release command with the next -// development version. -test("dev version bump syncs every tracked version", () => { - const fixture = createFixture(); - - const result = runScript(fixture, { CRAFT_NEW_VERSION: "0.49.0-dev.0" }); - - expect(result.status, result.stderr).toBe(0); - for (const path of ["package.json", "../mcp-server/package.json"]) { - expect(readVersion(join(fixture.cli, path))).toBe("0.49.0-dev.0"); - } - expect( - readVersion( - join(fixture.cli, "plugins/sentry-cli/.claude-plugin/plugin.json"), - ), - ).toBe("0.49.0"); - for (const path of ["SKILL.md", "references/auth.md"]) { - expect(readFileSync(join(fixture.cli, skill, path), "utf8")).toContain( - "version: 0.49.0-dev.0", - ); - } -}); diff --git a/packages/mcp-server/script/bundle.ts b/packages/mcp-server/script/bundle.ts index 522874301..5a0e8343f 100644 --- a/packages/mcp-server/script/bundle.ts +++ b/packages/mcp-server/script/bundle.ts @@ -95,7 +95,7 @@ const cliPkg = JSON.parse( ) as { version: string }; // Each CLI release publishes this package at the CLI's version, set by -// `packages/cli/script/bump-version.ts`. +// `versioning.files` in `.craft.yml`. if (pkg.version !== cliPkg.version) { console.error( `\nError: @sentry/mcp-server is at ${pkg.version} but the CLI is at ${cliPkg.version}. Both must share one version.\n`,