diff --git a/.codemod/verification b/.codemod/verification new file mode 100644 index 000000000..76804a3f4 --- /dev/null +++ b/.codemod/verification @@ -0,0 +1 @@ +codemod-verification=b6bab3ce-7770-40b0-9076-98bd50580f7d diff --git a/.github/workflows/publish-codemod.yml b/.github/workflows/publish-codemod.yml new file mode 100644 index 000000000..90427a9f9 --- /dev/null +++ b/.github/workflows/publish-codemod.yml @@ -0,0 +1,47 @@ +name: Publish codemod + +# Publishes packages/codemod-cli3to4 to the Codemod Registry as `@getsentry/cli3to4` +# whenever a push to main changes the `version` in its codemod.yaml. Codemod +# authenticates the publish with this workflow's GitHub Actions OIDC token, so +# no API key is stored here. The `@getsentry` scope must be verified once on +# codemod.com; see packages/codemod-cli3to4/README.md. + +on: + push: + branches: [main] + paths: + - "packages/codemod-cli3to4/codemod.yaml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + name: Check codemod version + runs-on: ubuntu-latest + outputs: + changed: ${{ steps.version.outputs.changed }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + - id: version + env: + BEFORE_SHA: ${{ github.event.before }} + run: node scripts/codemod-version-changed.mjs + + publish: + name: Publish to Codemod Registry + needs: check + # Only main is published, also for manual runs from another branch. + if: github.ref == 'refs/heads/main' && (github.event_name == 'workflow_dispatch' || needs.check.outputs.changed == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: codemod/publish-action@dd6c8dbc5ceb1a6146feba41481d88b43da50024 # v1 + with: + path: packages/codemod-cli3to4 diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 75785910b..cfdcf9ee5 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -16,6 +16,7 @@ "packages/cli/CHANGELOG.md", "packages/cli/AGENTS.md", "packages/mcp-server-mocks/src/fixtures/**", + "packages/codemod-cli3to4/tests/**", "**/worker-configuration.d.ts", "**/tsconfig*.json" ] diff --git a/.oxlintrc.json b/.oxlintrc.json index c4c752de0..08e9b21a1 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -13,6 +13,7 @@ "**/worker-configuration.d.ts", "**/tsconfig*.json", "packages/mcp-server-mocks/src/fixtures/**", + "packages/codemod-cli3to4/tests/**", "packages/cli/test/init-eval/templates/sveltekit-app/**" ], "rules": { diff --git a/AGENTS.md b/AGENTS.md index d5b1be82d..de0c60203 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ toolkit/ │ └── local/ # Sentry Local browser viewer ├── packages/ │ ├── cli/ # Sentry CLI (see packages/cli/AGENTS.md) +│ ├── codemod-cli3to4/ # @getsentry/cli3to4 codemod: CLI v3 → v4 (Codemod Registry) │ ├── logger/ # Structured logger (@sentry/logger on npm) │ ├── toolkit-core/ # Shared CLI/MCP primitives │ ├── mcp-core/ # Core MCP implementation (private) diff --git a/apps/cli-docs/src/content/docs/library-usage.md b/apps/cli-docs/src/content/docs/library-usage.md index 850af07c3..3153384d7 100644 --- a/apps/cli-docs/src/content/docs/library-usage.md +++ b/apps/cli-docs/src/content/docs/library-usage.md @@ -120,6 +120,22 @@ const help = await sdk.run("help", "issue"); ``` +## Version and executable path + +`getVersion()` returns the installed `sentry` package version, and `getPath()` +returns the absolute path to its `sentry` executable. Both are also available +as `createSentrySDK.getVersion()` and `createSentrySDK.getPath()`, matching +v3's static `SentryCli` methods. + +```typescript +import { execFileSync } from "node:child_process"; +import { getPath, getVersion } from "sentry"; + +console.log(getVersion()); +// The executable is a Node.js script, so run it with Node.js on every platform +execFileSync(process.execPath, [getPath(), "--version"]); +``` + ## Authentication The `token` option provides an auth token for the current invocation. When diff --git a/apps/cli-docs/src/content/docs/migrating-from-v3.md b/apps/cli-docs/src/content/docs/migrating-from-v3.md index 549834123..b8e3c8e09 100644 --- a/apps/cli-docs/src/content/docs/migrating-from-v3.md +++ b/apps/cli-docs/src/content/docs/migrating-from-v3.md @@ -418,7 +418,7 @@ v3's `@sentry/cli` package exported a `SentryCli` class for programmatic use: ```js // v3 -const SentryCli = require("@sentry/cli"); +const { SentryCli } = require("@sentry/cli"); const cli = new SentryCli(null, { authToken: process.env.SENTRY_AUTH_TOKEN }); await cli.releases.new("1.0.0"); await cli.releases.uploadSourceMaps("1.0.0", { include: ["./dist"] }); @@ -457,13 +457,15 @@ Mapping: | v3 (`@sentry/cli`) | v4 (`sentry`) | |--------------------|---------------| | `new SentryCli(configFile, { authToken })` | `createSentrySDK({ token })` (`configFile` dropped) | -| `cli.releases.new(v)` | `sdk.release.create({ orgVersion: v })` | +| `cli.releases.new(v, { projects })` | `sdk.release.create({ orgVersion: v, project: projects.join(",") })` | | `cli.releases.finalize(v)` | `sdk.release.finalize({ orgVersion: v })` | | `cli.releases.setCommits(v, o)` | `sdk.release["set-commits"]({ orgVersion: v, ...o })` | | `cli.releases.uploadSourceMaps(v, { include })` | `sdk.sourcemap.upload({ directory, release: v })` | | `cli.releases.newDeploy(v, { env, name, url })` | `sdk.release.deploy({ orgVersion: v, environment: env, name, url })` | | `cli.releases.proposeVersion()` | `sdk.release["propose-version"]()` | | `cli.execute(args)` | `sdk.run(...args)` | +| `SentryCli.getVersion()` | `getVersion()` (also `createSentrySDK.getVersion()`) | +| `SentryCli.getPath()` | `getPath()` (also `createSentrySDK.getPath()`); a Node.js script, not a native binary | Note the argument reshaping: the v3 `uploadSourceMaps` `include` array becomes the `directory` argument of `sourcemap.upload`, and the release is optional @@ -476,15 +478,15 @@ To automate the mechanical parts of this migration, run the constructor, and method chain, and inserts `// TODO(sentry-v4): …` comments where option shapes changed and need a manual check). -For now, run it from the CLI repo's source — clone the repo, then point the +For now, run it from the toolkit repo's source — clone the repo, then point the Codemod runner at the transform and your project directory: ```bash -git clone --depth 1 https://github.com/getsentry/cli /tmp/sentry-cli-src +git clone --depth 1 https://github.com/getsentry/toolkit /tmp/sentry-toolkit-src # From your project root (-t . targets the current directory) npx codemod@latest jssg run --language typescript \ - /tmp/sentry-cli-src/codemods/sentry-v3-to-v4/scripts/codemod.ts -t . + /tmp/sentry-toolkit-src/packages/codemod-cli3to4/scripts/codemod.ts -t . ``` :::note @@ -492,14 +494,15 @@ Once the codemod is published to the [Codemod registry](https://codemod.com), th shorter form below will work — until then, use the run-from-source command above: ```bash -npx codemod@latest @sentry/cli-v3-to-v4 +npx codemod@latest @getsentry/cli3to4 ``` ::: -It rewrites `.js`/`.ts` files in place. Review the diff afterward — argument +It rewrites `.js`/`.ts` files in place (the registry form also covers +`.jsx`/`.tsx`). Review the diff afterward — argument shapes differ (especially for `uploadSourceMaps` and `newDeploy`), so the codemod flags those rather than guessing. See -[`codemods/sentry-v3-to-v4`](https://github.com/getsentry/cli/tree/main/codemods/sentry-v3-to-v4) +[`packages/codemod-cli3to4`](https://github.com/getsentry/toolkit/tree/main/packages/codemod-cli3to4) for the source and test fixtures. ## Output and scripting diff --git a/package.json b/package.json index 9b679d6cb..d87e0d299 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "scripts": { "docs:check": "node scripts/check-doc-links.mjs", - "test:ci-projects": "node --test scripts/ci-projects.test.mjs scripts/check-workspace-deps.test.mjs scripts/cli-nightly-version.test.mjs scripts/cli-nightly-watchdog.test.mjs scripts/cloudflare-deployment.test.mjs scripts/deploy-workflow.test.mjs scripts/migrate-cloudflare-token.test.mjs scripts/oxc-config.test.mjs scripts/cli-lint-rules.test.mjs scripts/workflow-policy.test.mjs", + "test:ci-projects": "node --test scripts/ci-projects.test.mjs scripts/check-workspace-deps.test.mjs scripts/cli-nightly-version.test.mjs scripts/cli-nightly-watchdog.test.mjs scripts/codemod-version-changed.test.mjs scripts/cloudflare-deployment.test.mjs scripts/deploy-workflow.test.mjs scripts/migrate-cloudflare-token.test.mjs scripts/oxc-config.test.mjs scripts/cli-lint-rules.test.mjs scripts/workflow-policy.test.mjs", "dev": "pnpm --filter '@sentry/mcp-cloudflare...' --if-present run build && dotenv -e .env -e .env.local -- pnpm --parallel --filter @sentry/mcp-cloudflare --filter @sentry/mcp-core --filter @sentry/mcp-server-mocks --if-present run dev", "dev:stdio": "pnpm --filter '@sentry/mcp-core...' --if-present run build 1>&2 && dotenv -e .env -e .env.local -- pnpm --silent --dir packages/cli run cli mcp", "build": "dotenv -e .env -e .env.local -- pnpm -r --filter '!sentry' --filter '!sentry-cli-docs' --if-present run build", diff --git a/packages/cli/script/sdk-declarations.ts b/packages/cli/script/sdk-declarations.ts index dbfd1a308..1d98cdc61 100644 --- a/packages/cli/script/sdk-declarations.ts +++ b/packages/cli/script/sdk-declarations.ts @@ -62,6 +62,18 @@ export declare function createSentrySDK(options?: SentryOptions): SentrySDK & { run(...args: string[]): Promise | AsyncIterable; }; +/** The version of this \`sentry\` package. */ +export declare function getVersion(): string; + +/** Absolute path to this package's \`sentry\` executable (a Node.js script). */ +export declare function getPath(): string; + +/** v3's static \`SentryCli.getVersion()\` / \`SentryCli.getPath()\`. */ +export declare namespace createSentrySDK { + export function getVersion(): string; + export function getPath(): string; +} + export default createSentrySDK; `; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 646ce5ef2..f04d37cf6 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -20,6 +20,8 @@ * @module */ +import { fileURLToPath } from "node:url"; +import { CLI_VERSION } from "./lib/constants.js"; import { buildInvoker, buildRunner } from "./lib/sdk-invoke.js"; import { createSDKMethods } from "./sdk.generated.js"; @@ -59,4 +61,22 @@ export function createSentrySDK( return Object.assign(methods, { run }); } +/** The version of this `sentry` package (v3's static `SentryCli.getVersion()`). */ +export function getVersion(): string { + return CLI_VERSION; +} + +/** + * Absolute path to this package's `sentry` executable (v3's static + * `SentryCli.getPath()`). It is a Node.js script: spawn it with + * `process.execPath` on Windows, where it can't run on its own. + */ +export function getPath(): string { + return fileURLToPath(new URL("./bin.cjs", import.meta.url)); +} + +// v3 exposed these as static methods on the class; keep that spelling working. +createSentrySDK.getVersion = getVersion; +createSentrySDK.getPath = getPath; + export default createSentrySDK; diff --git a/packages/cli/test/e2e/library.test.ts b/packages/cli/test/e2e/library.test.ts index 076114b81..16582d66b 100644 --- a/packages/cli/test/e2e/library.test.ts +++ b/packages/cli/test/e2e/library.test.ts @@ -144,6 +144,23 @@ describe("library mode (bundled)", () => { expect(stdout.trim()).toBe("function"); }); + test("getVersion()/getPath() match v3's statics", async () => { + const { stdout } = await runNodeScriptOk(` + const mod = require('./dist/index.cjs'); + const { existsSync } = require('node:fs'); + const path = mod.getPath(); + console.log(JSON.stringify({ + version: mod.getVersion(), + bin: path === require('node:path').resolve('dist/bin.cjs') && existsSync(path), + statics: mod.default.getVersion === mod.getVersion && mod.default.getPath === mod.getPath, + })); + `); + const result = JSON.parse(stdout.trim()); + expect(result.version).toMatch(/^\d+\.\d+\.\d+/); + expect(result.bin).toBe(true); + expect(result.statics).toBe(true); + }); + // --- run() escape hatch --- test("sdk.run('--version') returns version string", async () => { diff --git a/packages/cli/test/lib/index.test.ts b/packages/cli/test/lib/index.test.ts index c0b508120..a5f2c85da 100644 --- a/packages/cli/test/lib/index.test.ts +++ b/packages/cli/test/lib/index.test.ts @@ -1,5 +1,10 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import createSentrySDK, { SentryError } from "../../src/index.js"; +import createSentrySDK, { + getPath, + getVersion, + SentryError, +} from "../../src/index.js"; +import { CLI_VERSION } from "../../src/lib/constants.js"; import { setAuthToken } from "../../src/lib/db/auth.js"; import { mockFetch, useTestConfigDir } from "../helpers.js"; @@ -169,3 +174,18 @@ describe("createSentrySDK() library API", () => { expect(Symbol.asyncIterator in (result as object)).toBe(true); }); }); + +describe("v3 static helpers", () => { + test("getVersion() returns the CLI version", () => { + expect(getVersion()).toBe(CLI_VERSION); + }); + + test("getPath() points at bin.cjs next to the entry point", () => { + expect(getPath()).toMatch(/[\\/]bin\.cjs$/); + }); + + test("both are also static members of createSentrySDK, as on v3's class", () => { + expect(createSentrySDK.getVersion).toBe(getVersion); + expect(createSentrySDK.getPath).toBe(getPath); + }); +}); diff --git a/packages/codemod-cli3to4/.gitignore b/packages/codemod-cli3to4/.gitignore new file mode 100644 index 000000000..196d8c22b --- /dev/null +++ b/packages/codemod-cli3to4/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +target/ +*.tgz diff --git a/packages/codemod-cli3to4/README.md b/packages/codemod-cli3to4/README.md new file mode 100644 index 000000000..e360363c7 --- /dev/null +++ b/packages/codemod-cli3to4/README.md @@ -0,0 +1,68 @@ +# `@getsentry/cli3to4` + +A [Codemod](https://codemod.com) that migrates programmatic **`@sentry/cli` v3** +usage (the `SentryCli` class) to the **v4 `sentry` package** (`createSentrySDK` +factory + typed command methods). + +It is referenced from the CLI's +[migration guide](https://cli.sentry.dev/migrating-from-v3/). + +## Usage + +```bash +npx codemod @getsentry/cli3to4 +``` + +Run it from your project root; it rewrites `.js`/`.ts`/`.jsx`/`.tsx` files (and +their `.mjs`/`.cjs`/`.mts`/`.cts` variants) in place. Review the diff afterward — +the codemod inserts `// TODO(sentry-v4): …` comments wherever an option shape +changed and needs a manual decision (it flags rather than guesses). + +## What it does + +- Rewrites the module specifier `@sentry/cli` → `sentry`, **keeping your local + binding name** (so re-exports and pass-throughs stay valid). v3's named + export becomes v4's factory: `import { SentryCli }` → + `import { createSentrySDK as SentryCli }`, `const { SentryCli } = require(…)` + → `const { createSentrySDK: SentryCli } = require(…)`, and `X.SentryCli` → + `X.createSentrySDK`. The v2 default-export shapes (`import X from`, bare + `require`) map to v4's default export. +- `new SentryCli(configFile?, options?)` → `SentryCli(options?)` — drops `new` + (v4 exports a factory, not a class) and the removed `configFile` positional, + and renames the `authToken` option to `token`. +- Maps the release/sourcemap methods to the typed v4 SDK: + `releases.new`→`release.create`, `releases.finalize`→`release.finalize`, + `releases.setCommits`→`release["set-commits"]`, + `releases.newDeploy`→`release.deploy` (renaming `env` to `environment`), + `releases.listDeploys`→`release.deploys`, + `releases.proposeVersion`→`release["propose-version"]`, + `releases.uploadSourceMaps`→`sourcemap.upload`, + `sourceMaps.inject`→`sourcemap.inject`, and `execute([...])`→`run(...)`. + The release version moves into the options object as `orgVersion` (or + `release` for `sourcemap.upload`). +- Only touches files that actually import `@sentry/cli`, and only rewrites + method calls on instances it can see created via `new SentryCli(...)`. + +## Development + +```bash +# Run the test fixtures (tests//input.ts + expected.ts) +pnpm test + +# Type-check the transform +pnpm run typecheck + +# Update expected fixtures after an intentional change +pnpm dlx codemod@1.12.7 jssg test -l typescript ./scripts/codemod.ts -u +``` + +## Publishing + +Published to the [Codemod Registry](https://codemod.com/registry) under the +`@getsentry` scope. Codemod verifies that scope against this repository +(`.codemod/verification` at the repo root). To release, bump `version` in +`codemod.yaml` and merge to `main`. +[`.github/workflows/publish-codemod.yml`](../../.github/workflows/publish-codemod.yml) +runs on pushes to `main` that touch `codemod.yaml` and publishes with GitHub +Actions OIDC when the version changed. You can also run it by hand from the +Actions tab. diff --git a/packages/codemod-cli3to4/codemod.yaml b/packages/codemod-cli3to4/codemod.yaml new file mode 100644 index 000000000..703c60058 --- /dev/null +++ b/packages/codemod-cli3to4/codemod.yaml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +name: "@getsentry/cli3to4" +version: "0.1.0" +description: "Migrate @sentry/cli v3 SDK usage to the v4 sentry package" +author: "Sentry" +license: "MIT" +repository: "https://github.com/getsentry/toolkit" + +targets: + # Keep this aligned with the files matched in workflow.yaml. + languages: ["typescript", "tsx"] + +keywords: ["sentry", "sentry-cli", "migration", "v3", "v4"] + +registry: + access: "public" + visibility: "public" + +capabilities: [] + +workflows: + - name: main + default: true + path: workflow.yaml diff --git a/packages/codemod-cli3to4/package.json b/packages/codemod-cli3to4/package.json new file mode 100644 index 000000000..6ebc5ead9 --- /dev/null +++ b/packages/codemod-cli3to4/package.json @@ -0,0 +1,16 @@ +{ + "name": "@sentry/cli3to4", + "version": "0.1.0", + "private": true, + "description": "Codemod that migrates @sentry/cli v3 SDK usage to the v4 sentry package", + "type": "module", + "license": "MIT", + "scripts": { + "test": "pnpm dlx codemod@1.12.7 jssg test -l typescript ./scripts/codemod.ts", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@codemod.com/jssg-types": "1.6.3", + "typescript": "catalog:" + } +} diff --git a/packages/codemod-cli3to4/scripts/codemod.ts b/packages/codemod-cli3to4/scripts/codemod.ts new file mode 100644 index 000000000..e402f3146 --- /dev/null +++ b/packages/codemod-cli3to4/scripts/codemod.ts @@ -0,0 +1,591 @@ +import type { Codemod, Edit, SgNode } from "codemod:ast-grep"; +import type JS from "codemod:ast-grep/langs/javascript"; +import type TS from "codemod:ast-grep/langs/typescript"; +import type TSX from "codemod:ast-grep/langs/tsx"; + +/** + * Migrates programmatic `@sentry/cli` v3 (`SentryCli` class) usage to the v4 + * `sentry` package (`createSentrySDK` factory + typed command methods). + * + * The transform is intentionally conservative: it only touches files that + * actually import `@sentry/cli`, keeps the user's binding name, and drops a + * `// TODO(sentry-v4)` breadcrumb wherever an option shape changed and needs a + * manual check rather than guessing. + */ + +type L = JS | TS | TSX; +type Node = SgNode; + +/** v3 `releases.` → v4 `[route, target]`. */ +const METHOD_MAP: Record = { + new: ["release", "create"], + finalize: ["release", "finalize"], + setCommits: ["release", "set-commits"], + uploadSourceMaps: ["sourcemap", "upload"], + proposeVersion: ["release", "propose-version"], + newDeploy: ["release", "deploy"], + listDeploys: ["release", "deploys"], +}; + +/** How each method's positional args fold into the v4 options object. */ +const RESHAPE: Record< + string, + { + key: string; + /** Merge the v3 options argument into the v4 options object. */ + spread?: boolean; + /** v3 → v4 key renames applied inside an object-literal options arg. */ + rename?: Record; + todo?: string; + } +> = { + // v3 `new(release, { projects })`: see `projectArg` for the v4 `project`. + new: { key: "orgVersion" }, + finalize: { key: "orgVersion" }, + setCommits: { + key: "orgVersion", + spread: true, + todo: 'verify set-commits options: v3 `repo`/`commit`/`previousCommit` become one `commit: "REPO@PREV..SHA"` string; `ignoreMissing`/`ignoreEmpty` were dropped; `auto` is kept', + }, + newDeploy: { + key: "orgVersion", + spread: true, + rename: { env: "environment" }, + todo: "verify deploy options: v3 `env` is v4 `environment`; `started`/`finished` are ISO 8601 strings and `time` is a string of seconds (v3 took numbers); `projects` was dropped", + }, + listDeploys: { key: "orgVersion" }, + // No spread: v3 `include` is a path array, but v4 `sourcemap.upload` takes a + // single `directory` string — spreading would emit an invalid `include` key. + uploadSourceMaps: { + key: "release", + todo: "sourcemaps are debug-ID-first in v4: set the `directory` option to your bundle output dir (v3 `include` was a path array — run one upload per directory) and review the remaining options", + }, +}; + +const isNullish = (n: Node) => + n.kind() === "null" || + (n.kind() === "identifier" && n.text() === "undefined"); +const identSafe = (s: string) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s); +const accessor = (name: string) => + identSafe(name) ? `.${name}` : `[${JSON.stringify(name)}]`; +const unquote = (s: string) => s.replace(/^['"]|['"]$/g, ""); + +/** + * The v4 `project` value (one comma-separated string) for v3's + * `releases.new(release, opts)` options, whose only key was `projects` + * (a string array). `null` when `opts` has other keys to review by hand. + */ +/** `n`'s text, parenthesized unless it can take a `.member` suffix as-is. */ +const operand = (n: Node) => + /^(identifier|member_expression|subscript_expression|call_expression|array|parenthesized_expression|this)$/.test( + n.kind(), + ) + ? n.text() + : `(${n.text()})`; + +const projectArg = (opts: Node): string | null => { + if (opts.kind() !== "object") return `${operand(opts)}.projects?.join(",")`; + const props = opts.children().filter((c) => c.isNamed()); + if (props.length !== 1) return null; + const [p] = props; + if ( + p!.kind() === "shorthand_property_identifier" && + p!.text() === "projects" + ) { + return `projects.join(",")`; + } + const k = p!.kind() === "pair" ? p!.field("key") : null; + const v = p!.field("value"); + if (!k || !v || unquote(k.text()) !== "projects") return null; + const items = + v.kind() === "array" ? v.children().filter((c) => c.isNamed()) : []; + // `["web", "api"]` → `"web,api"`; anything else is joined at runtime. + if ( + items.length && + items.every((i) => i.kind() === "string" && !/[\\,]/.test(i.text())) + ) { + return JSON.stringify(items.map((i) => unquote(i.text())).join(",")); + } + return `${operand(v)}.join(",")`; +}; + +const codemod: Codemod = async (root) => { + const rootNode = root.root(); + const source = root.source(); + const edits: Edit[] = []; + + /** Local names bound to the v3 SentryCli class (default import / require). */ + const bindings = new Set(); + /** Namespace-import names (`import * as X`) whose `.default` is the v3 class. */ + const nsBindings = new Set(); + /** Variable names holding an SDK instance (`const cli = new SentryCli()`). */ + const instanceNames = new Set(); + + const named = (n: Node) => n.isNamed(); + const argsOf = (call: Node): Node[] => { + const a = call.field("arguments"); + return a ? a.children().filter(named) : []; + }; + + /** Index of the start of the line containing `index`. */ + const lineStart = (index: number) => source.lastIndexOf("\n", index - 1) + 1; + + /** Insert a leading `// TODO(sentry-v4): …` above the enclosing statement. */ + const todo = (node: Node, msg: string): Edit => { + let p: Node | null = node; + while (p && !/(_statement|declaration)$/.test(p.kind())) { + p = p.parent(); + } + const start = (p ?? node).range().start.index; + const ls = lineStart(start); + const indent = source.slice(ls, start); + return { + startPos: ls, + endPos: ls, + insertedText: `${indent}// TODO(sentry-v4): ${msg}\n`, + }; + }; + + const replaceNode = (node: Node, text: string): Edit => ({ + startPos: node.range().start.index, + endPos: node.range().end.index, + insertedText: text, + }); + + /** An object literal's text with top-level keys renamed per `rename`. */ + const renameKeys = (obj: Node, rename: Record): string => { + const base = obj.range().start.index; + const out: [number, number, string][] = []; + for (const c of obj.children()) { + if (c.kind() === "pair") { + const k = c.field("key"); + const to = k && rename[unquote(k.text())]; + if (k && to) out.push([k.range().start.index, k.range().end.index, to]); + } else if (c.kind() === "shorthand_property_identifier") { + const to = rename[c.text()]; + if (to) { + out.push([ + c.range().start.index, + c.range().end.index, + `${to}: ${c.text()}`, + ]); + } + } + } + let text = obj.text(); + for (const [start, end, to] of out.reverse()) { + text = text.slice(0, start - base) + to + text.slice(end - base); + } + return text; + }; + + /** Empty a call's argument list, keeping the parens: `X(null,\n)` → `X()`. */ + const clearArgs = (call: Node): Edit => { + const r = call.field("arguments")!.range(); + return { + startPos: r.start.index + 1, + endPos: r.end.index - 1, + insertedText: "", + }; + }; + + // 1) Rewrite the module specifier, keeping the user's binding name. + // v3 exports the class as `SentryCli` (`import { SentryCli }`); v2 set + // `module.exports` to it (`import X from` / bare `require`). v4 exports + // the `createSentrySDK` factory as both its default and a named export. + const rewriteSource = (src: Node) => { + const q = src.text()[0]; + edits.push(replaceNode(src, `${q}sentry${q}`)); + }; + /** Names used as `X.SentryCli`, i.e. bindings holding the v3 module object. */ + const memberObjects = new Set( + rootNode + .findAll({ rule: { pattern: "$OBJ.SentryCli" } }) + .map((m) => m.getMatch("OBJ")?.text() ?? ""), + ); + + // ESM: `import X from`, `import * as X from`, `import { SentryCli } from`. + for (const imp of rootNode.findAll({ + rule: { + kind: "import_statement", + has: { field: "source", regex: "@sentry/cli" }, + }, + })) { + const src = imp.field("source"); + if (!src || unquote(src.text()) !== "@sentry/cli") continue; + const clause = imp.children().find((c) => c.kind() === "import_clause"); + const def = clause?.children().find((c) => c.kind() === "identifier"); + const nsId = clause + ?.children() + .find((c) => c.kind() === "namespace_import") + ?.children() + .find((c) => c.kind() === "identifier"); + if (def) bindings.add(def.text()); + if (nsId) { + // `new X.SentryCli()` (v3) or `new X.default()` (v2 via interop). + bindings.add(nsId.text()); + nsBindings.add(nsId.text()); + } + for (const spec of imp.findAll({ rule: { kind: "import_specifier" } })) { + const name = spec.field("name")?.text(); + const local = spec.field("alias")?.text() ?? name; + if (!name || !local) continue; + const typeOnly = /^type\s/.test(spec.text()) ? "type " : ""; + if (name === "SentryCli") { + bindings.add(local); + edits.push(replaceNode(spec, `${typeOnly}createSentrySDK as ${local}`)); + } else if (name === "SentryCliOptions") { + edits.push(replaceNode(spec, `${typeOnly}SentryOptions as ${local}`)); + } else { + edits.push( + todo( + imp, + `v4 has no \`${name}\` export; use the parameter types of the v4 SDK methods instead`, + ), + ); + } + } + rewriteSource(src); + } + // CJS: `require("@sentry/cli")` bare, `.default`, `.SentryCli`, or + // destructured (`const { SentryCli } = require(…)`). + for (const call of rootNode.findAll({ rule: { pattern: "require($SRC)" } })) { + const src = call.getMatch("SRC"); + if (!src || unquote(src.text()) !== "@sentry/cli") continue; + const parent = call.parent(); + const member = parent?.kind() === "member_expression" ? parent : null; + // The name the value lands in: `const X = …` or `X = …`, also through + // parens and ternary branches (`const X = c ? require(…) : null`). + let value: Node = member ?? call; + let decl = value.parent(); + while ( + decl && + (decl.kind() === "parenthesized_expression" || + (decl.kind() === "ternary_expression" && + decl.field("condition")?.id() !== value.id())) + ) { + value = decl; + decl = decl.parent(); + } + const id = + decl?.kind() === "variable_declarator" + ? decl.field("name") + : decl?.kind() === "assignment_expression" && + decl.field("right")?.id() === value.id() + ? decl.field("left") + : null; + + if (id?.kind() === "object_pattern") { + for (const c of id.children()) { + if ( + c.kind() === "shorthand_property_identifier_pattern" && + c.text() === "SentryCli" + ) { + bindings.add("SentryCli"); + edits.push(replaceNode(c, "createSentrySDK: SentryCli")); + } else if (c.kind() === "pair_pattern") { + const k = c.field("key"); + const v = c.field("value"); + if (k && unquote(k.text()) === "SentryCli" && v) { + bindings.add(v.text()); + edits.push(replaceNode(k, "createSentrySDK")); + } + } + } + rewriteSource(src); + } else if (member) { + // `.default` stays valid; `.SentryCli` becomes `.createSentrySDK` + // (rewritten with the other `X.SentryCli` member accesses below). + if (id?.kind() === "identifier") bindings.add(id.text()); + rewriteSource(src); + } else if (id?.kind() === "identifier" && memberObjects.has(id.text())) { + // v3: the module object, used as `X.SentryCli`. + bindings.add(id.text()); + nsBindings.add(id.text()); + rewriteSource(src); + } else { + // v2: `module.exports` was the class itself; v4's CJS module object + // exposes the factory as `.default`/`createSentrySDK`, so add `.default`. + if (id?.kind() === "identifier") bindings.add(id.text()); + const q = src.text()[0]; + edits.push(replaceNode(call, `require(${q}sentry${q}).default`)); + } + } + + // `X.SentryCli` on the v3 module object → `X.createSentrySDK`. + const isRequire = (n: Node) => + /^require\(\s*["']@sentry\/cli["']\s*\)$/.test(n.text()); + const isModuleMember = (obj: Node | null) => + !!obj && (nsBindings.has(obj.text()) || isRequire(obj)); + for (const m of rootNode.findAll({ rule: { pattern: "$OBJ.SentryCli" } })) { + const prop = m.field("property"); + if (prop && isModuleMember(m.getMatch("OBJ"))) { + edits.push(replaceNode(prop, "createSentrySDK")); + } + } + + // v3's static `getVersion()`/`getPath()` are static members of the v4 + // factory too, so the calls carry over. `getPath()` now points at a + // Node.js script rather than a native binary. + for (const call of rootNode.findAll({ rule: { pattern: "$B.getPath()" } })) { + const b = call.getMatch("B"); + if ( + b && + (bindings.has(b.text()) || + isModuleMember(b) || + isModuleMember(b.field("object"))) + ) { + edits.push( + todo( + call, + "v4 `getPath()` is a Node.js script, not a native binary: spawn it as `execFile(process.execPath, [getPath(), ...args])`, or call the SDK methods instead", + ), + ); + } + } + + // 2) Collect instance variable names from `new (...)`. + // Matches `new X(...)` for any binding, plus `new X.default(...)` when X is + // a namespace import (the v3 class is the module's default export). + const isCtor = (c: Node | null): boolean => { + while (c?.kind() === "parenthesized_expression") { + c = c.children().find(named) ?? null; + } + if (!c) return false; + // `new (require("@sentry/cli"))()`: v2's module was the class itself. + if (bindings.has(c.text()) || isRequire(c)) return true; + if (c.kind() === "member_expression") { + const obj = c.field("object"); + const prop = c.field("property"); + return ( + !!obj && + !!prop && + (prop.text() === "default" || prop.text() === "SentryCli") && + isModuleMember(obj) + ); + } + return false; + }; + const newExprs = rootNode + .findAll({ rule: { pattern: "new $C($$$A)" } }) + .filter((n) => isCtor(n.field("constructor"))); + for (const ne of newExprs) { + const p = ne.parent(); + if (p?.kind() === "variable_declarator") { + const id = p.field("name"); + if (id?.kind() === "identifier") instanceNames.add(id.text()); + } else if (p?.kind() === "assignment_expression") { + const l = p.field("left"); + if (l?.kind() === "identifier") instanceNames.add(l.text()); + } + } + const newExprIds = new Set(newExprs.map((n) => n.id())); + /** An SDK instance: a variable from step 2, or `new X()` inline. */ + const isInstance = (recv: Node | null): boolean => { + while (recv?.kind() === "parenthesized_expression") { + recv = recv.children().find(named) ?? null; + } + if (!recv) return false; + if (recv.kind() === "new_expression") return newExprIds.has(recv.id()); + return recv.kind() === "identifier" && instanceNames.has(recv.text()); + }; + /** Replace a call after its receiver, so edits inside the receiver (an + * inline `new X(…)`) still apply. */ + const replaceAfter = (recv: Node, call: Node, text: string): Edit => ({ + startPos: recv.range().end.index, + endPos: call.range().end.index, + insertedText: text, + }); + + // 3) Constructor: `new X(configFile?, options?)` → `X(options?)`. + for (const ne of newExprs) { + const callee = ne.field("constructor"); + if (!callee) continue; + // Drop `new ` (v4's default export is a factory function, not a class). + edits.push({ + startPos: ne.range().start.index, + endPos: callee.range().start.index, + insertedText: "", + }); + + const a = argsOf(ne); + let options: Node | null = null; + let ambiguous = false; + if (a.length >= 2) { + if (isNullish(a[1]!)) { + // `new X(cfg, null)` → `X()` (also drops any trailing comma). + edits.push(clearArgs(ne)); + } else { + // Drop the configFile positional (removed in v4). + edits.push({ + startPos: a[0]!.range().start.index, + endPos: a[1]!.range().start.index, + insertedText: "", + }); + options = a[1]!; + } + } else if (a.length === 1) { + const arg = a[0]!; + if (isNullish(arg)) { + edits.push(clearArgs(ne)); // `new X(null)` → `X()` + } else if (arg.kind() === "object") { + options = arg; + } else { + options = arg; + ambiguous = true; + } + } + + if (options?.kind() === "object") { + // Rename authToken → token in place. + const keys: string[] = []; + for (const c of options.children()) { + if (c.kind() === "pair") { + const k = c.field("key"); + const kn = k ? unquote(k.text()) : ""; + if (kn) keys.push(kn); + if (k && kn === "authToken") edits.push(replaceNode(k, "token")); + } else if (c.kind() === "shorthand_property_identifier") { + keys.push(c.text()); + if (c.text() === "authToken") { + // `{ authToken }` → `{ token: authToken }` (keep the binding). + edits.push(replaceNode(c, "token: authToken")); + } + } + } + // v3 SentryCliOptions keys with no direct v4 `SentryOptions` equivalent. + // (`token`/`url`/`org`/`project`/`headers` ARE first-class v4 options, so + // they're fine as-is; only flag the ones that need attention.) + const unmapped = keys.filter((k) => + ["apiKey", "dsn", "silent", "customHeader", "vcsRemote"].includes(k), + ); + if (unmapped.length) { + edits.push( + todo( + ne, + `verify these v3 options — they have no direct v4 SentryOptions equivalent: ${unmapped.join(", ")} (apiKey → rename to \`token\`; customHeader → move the "Name: value" string into the \`headers\` object; dsn → use SENTRY_DSN env; silent/vcsRemote were dropped)`, + ), + ); + } + } else if (ambiguous) { + edits.push( + todo( + ne, + "verify this argument: v3's first constructor param was a configFile path (removed in v4); v4 takes an options object. Drop it if it's a config path, or map authToken→token if it's options", + ), + ); + } else if (options) { + edits.push( + todo( + ne, + "v4 renamed the `authToken` option to `token` — update it inside the options object passed here", + ), + ); + } + } + + // 4a) `.execute([...], live?)` → `.run(...args)`. + for (const call of rootNode.findAll({ + rule: { pattern: "$RECV.execute($$$A)" }, + })) { + const recv = call.getMatch("RECV"); + if (!isInstance(recv)) continue; + const a = argsOf(call); + let runArgs = ""; + if (a[0]?.kind() === "array") { + runArgs = a[0] + .children() + .filter(named) + .map((c) => c.text()) + .join(", "); + } else if (a[0]) { + runArgs = `...${a[0].text()}`; + } + edits.push( + todo( + call, + "run(...) passes raw CLI args verbatim; remap v3 command names to v4 (releases→release, new→create, login→auth login, …) and verify flags. NOTE: v3's second `execute` arg (`live`) is dropped — v4 `run()` always captures and returns output (v3 `live:false` behavior); if you passed `live:true`/`'rejectOnError'` for streamed stdio, adjust accordingly", + ), + ); + edits.push(replaceAfter(recv!, call, `.run(${runArgs})`)); + } + + // 4b) `.releases.(...)` → `..({…})`. + for (const call of rootNode.findAll({ + rule: { pattern: "$RECV.releases.$METHOD($$$A)" }, + })) { + const recv = call.getMatch("RECV"); + const methodNode = call.getMatch("METHOD"); + if (!recv || !methodNode || !isInstance(recv)) continue; + const method = methodNode.text(); + const a = argsOf(call); + + const map = METHOD_MAP[method]; + if (!map) continue; + const [route, target] = map; + const reshape = RESHAPE[method]; + + let argsOut: string; + const project = method === "new" && a[1] ? projectArg(a[1]) : undefined; + if (method === "new" && a[1] && project === null) { + edits.push( + todo( + call, + "map v3 `projects` (array) to v4 `project` (comma-separated string) by hand", + ), + ); + } + if (project && a[0]) { + argsOut = `{ orgVersion: ${a[0].text()}, project: ${project} }`; + } else if (reshape) { + const lead = a[0] ? `${reshape.key}: ${a[0].text()}` : ""; + const opts = reshape.spread ? a[1] : undefined; + if (opts?.kind() === "object" && opts.children().some(named)) { + // Keep the user's object text verbatim (layout and comments, so a + // trailing `// …` can't swallow the closing brace); prepend the key. + const text = reshape.rename + ? renameKeys(opts, reshape.rename) + : opts.text(); + argsOut = `{ ${lead ? `${lead},` : ""}${text.slice(1)}`; + } else { + const parts = [lead]; + if (opts && opts.kind() !== "object") parts.push(`...${opts.text()}`); + const kept = parts.filter(Boolean); + argsOut = kept.length ? `{ ${kept.join(", ")} }` : ""; + } + } else { + argsOut = a.map((n) => n.text()).join(", "); + } + + if (reshape?.todo) edits.push(todo(call, reshape.todo)); + edits.push( + replaceAfter( + recv, + call, + `${accessor(route)}${accessor(target)}(${argsOut})`, + ), + ); + } + + // 4c) `.sourceMaps.inject(opts)` → `.sourcemap.inject(opts)`. + for (const call of rootNode.findAll({ + rule: { pattern: "$RECV.sourceMaps.inject($$$A)" }, + })) { + const recv = call.getMatch("RECV"); + if (!recv || !isInstance(recv)) continue; + const args = argsOf(call) + .map((n) => n.text()) + .join(", "); + edits.push( + todo( + call, + "verify inject options: v4 takes one `directory` (v3 `paths` was an array — run once per directory), and `ignore`/`ext` are comma-separated strings; `ignoreFile`/`dryRun` are kept", + ), + ); + edits.push(replaceAfter(recv, call, `.sourcemap.inject(${args})`)); + } + + return edits.length ? rootNode.commitEdits(edits) : null; +}; + +export default codemod; diff --git a/packages/codemod-cli3to4/tests/authtoken-variable/expected.ts b/packages/codemod-cli3to4/tests/authtoken-variable/expected.ts new file mode 100644 index 000000000..9b48850da --- /dev/null +++ b/packages/codemod-cli3to4/tests/authtoken-variable/expected.ts @@ -0,0 +1,3 @@ +import SentryCli from "sentry"; +// TODO(sentry-v4): v4 renamed the `authToken` option to `token` — update it inside the options object passed here +const cli = SentryCli(opts); diff --git a/packages/codemod-cli3to4/tests/authtoken-variable/input.ts b/packages/codemod-cli3to4/tests/authtoken-variable/input.ts new file mode 100644 index 000000000..115f515aa --- /dev/null +++ b/packages/codemod-cli3to4/tests/authtoken-variable/input.ts @@ -0,0 +1,2 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(null, opts); diff --git a/packages/codemod-cli3to4/tests/chained-new/expected.ts b/packages/codemod-cli3to4/tests/chained-new/expected.ts new file mode 100644 index 000000000..2df163120 --- /dev/null +++ b/packages/codemod-cli3to4/tests/chained-new/expected.ts @@ -0,0 +1,4 @@ +import SentryCli from "sentry"; +await SentryCli({ token: t }).release.finalize({ orgVersion: "1.0.0" }); +// TODO(sentry-v4): run(...) passes raw CLI args verbatim; remap v3 command names to v4 (releases→release, new→create, login→auth login, …) and verify flags. NOTE: v3's second `execute` arg (`live`) is dropped — v4 `run()` always captures and returns output (v3 `live:false` behavior); if you passed `live:true`/`'rejectOnError'` for streamed stdio, adjust accordingly +await SentryCli().run("releases", "list"); diff --git a/packages/codemod-cli3to4/tests/chained-new/input.ts b/packages/codemod-cli3to4/tests/chained-new/input.ts new file mode 100644 index 000000000..a4016b5a4 --- /dev/null +++ b/packages/codemod-cli3to4/tests/chained-new/input.ts @@ -0,0 +1,3 @@ +import SentryCli from "@sentry/cli"; +await new SentryCli(null, { authToken: t }).releases.finalize("1.0.0"); +await new SentryCli().execute(["releases", "list"], false); diff --git a/packages/codemod-cli3to4/tests/cjs-destructure-alias/expected.ts b/packages/codemod-cli3to4/tests/cjs-destructure-alias/expected.ts new file mode 100644 index 000000000..d03cb91d1 --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-destructure-alias/expected.ts @@ -0,0 +1,2 @@ +const { createSentrySDK: Cli } = require("sentry"); +const cli = Cli(); diff --git a/packages/codemod-cli3to4/tests/cjs-destructure-alias/input.ts b/packages/codemod-cli3to4/tests/cjs-destructure-alias/input.ts new file mode 100644 index 000000000..ed882f3d1 --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-destructure-alias/input.ts @@ -0,0 +1,2 @@ +const { SentryCli: Cli } = require("@sentry/cli"); +const cli = new Cli(); diff --git a/packages/codemod-cli3to4/tests/cjs-destructure/expected.ts b/packages/codemod-cli3to4/tests/cjs-destructure/expected.ts new file mode 100644 index 000000000..c2c95882d --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-destructure/expected.ts @@ -0,0 +1,3 @@ +const { createSentrySDK: SentryCli } = require("sentry"); +const cli = SentryCli({ token: process.env.T }); +cli.release["propose-version"](); diff --git a/packages/codemod-cli3to4/tests/cjs-destructure/input.ts b/packages/codemod-cli3to4/tests/cjs-destructure/input.ts new file mode 100644 index 000000000..b11f2aecf --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-destructure/input.ts @@ -0,0 +1,3 @@ +const { SentryCli } = require("@sentry/cli"); +const cli = new SentryCli(null, { authToken: process.env.T }); +cli.releases.proposeVersion(); diff --git a/packages/codemod-cli3to4/tests/cjs-inline-construct/expected.ts b/packages/codemod-cli3to4/tests/cjs-inline-construct/expected.ts new file mode 100644 index 000000000..950620ae6 --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-inline-construct/expected.ts @@ -0,0 +1,2 @@ +const cli = (require("sentry").default)({ token: t }); +await cli.release.finalize({ orgVersion: "1.0.0" }); diff --git a/packages/codemod-cli3to4/tests/cjs-inline-construct/input.ts b/packages/codemod-cli3to4/tests/cjs-inline-construct/input.ts new file mode 100644 index 000000000..bae1e822a --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-inline-construct/input.ts @@ -0,0 +1,2 @@ +const cli = new (require("@sentry/cli"))(null, { authToken: t }); +await cli.releases.finalize("1.0.0"); diff --git a/packages/codemod-cli3to4/tests/cjs-inline-getpath/expected.ts b/packages/codemod-cli3to4/tests/cjs-inline-getpath/expected.ts new file mode 100644 index 000000000..c154c5160 --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-inline-getpath/expected.ts @@ -0,0 +1,3 @@ +const { execFileSync } = require("child_process"); +// TODO(sentry-v4): v4 `getPath()` is a Node.js script, not a native binary: spawn it as `execFile(process.execPath, [getPath(), ...args])`, or call the SDK methods instead +execFileSync(require("sentry").getPath(), ["--version"]); diff --git a/packages/codemod-cli3to4/tests/cjs-inline-getpath/input.ts b/packages/codemod-cli3to4/tests/cjs-inline-getpath/input.ts new file mode 100644 index 000000000..29b45004d --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-inline-getpath/input.ts @@ -0,0 +1,2 @@ +const { execFileSync } = require("child_process"); +execFileSync(require("@sentry/cli").getPath(), ["--version"]); diff --git a/packages/codemod-cli3to4/tests/cjs-member/expected.ts b/packages/codemod-cli3to4/tests/cjs-member/expected.ts new file mode 100644 index 000000000..a5b75722d --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-member/expected.ts @@ -0,0 +1,2 @@ +const SentryCli = require("sentry").createSentrySDK; +const cli = SentryCli(); diff --git a/packages/codemod-cli3to4/tests/cjs-member/input.ts b/packages/codemod-cli3to4/tests/cjs-member/input.ts new file mode 100644 index 000000000..488b6d3a3 --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-member/input.ts @@ -0,0 +1,2 @@ +const SentryCli = require("@sentry/cli").SentryCli; +const cli = new SentryCli(); diff --git a/packages/codemod-cli3to4/tests/cjs-module-object/expected.ts b/packages/codemod-cli3to4/tests/cjs-module-object/expected.ts new file mode 100644 index 000000000..5767fb739 --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-module-object/expected.ts @@ -0,0 +1,2 @@ +const sentry = require("sentry"); +const cli = sentry.createSentrySDK({ token: t }); diff --git a/packages/codemod-cli3to4/tests/cjs-module-object/input.ts b/packages/codemod-cli3to4/tests/cjs-module-object/input.ts new file mode 100644 index 000000000..85e77bf0a --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-module-object/input.ts @@ -0,0 +1,2 @@ +const sentry = require("@sentry/cli"); +const cli = new sentry.SentryCli(null, { authToken: t }); diff --git a/packages/codemod-cli3to4/tests/cjs-require-assign/expected.ts b/packages/codemod-cli3to4/tests/cjs-require-assign/expected.ts new file mode 100644 index 000000000..2ada6be1b --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-require-assign/expected.ts @@ -0,0 +1,4 @@ +let SentryCli; +SentryCli = require("sentry").default; +const cli = SentryCli({ token: t }); +await cli.release.finalize({ orgVersion: "1.0.0" }); diff --git a/packages/codemod-cli3to4/tests/cjs-require-assign/input.ts b/packages/codemod-cli3to4/tests/cjs-require-assign/input.ts new file mode 100644 index 000000000..fcb50038c --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-require-assign/input.ts @@ -0,0 +1,4 @@ +let SentryCli; +SentryCli = require("@sentry/cli"); +const cli = new SentryCli(null, { authToken: t }); +await cli.releases.finalize("1.0.0"); diff --git a/packages/codemod-cli3to4/tests/cjs-require-construct/expected.ts b/packages/codemod-cli3to4/tests/cjs-require-construct/expected.ts new file mode 100644 index 000000000..d797bb85f --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-require-construct/expected.ts @@ -0,0 +1,2 @@ +const SentryCli = require("sentry").default; +const cli = SentryCli({ token: t }); diff --git a/packages/codemod-cli3to4/tests/cjs-require-construct/input.ts b/packages/codemod-cli3to4/tests/cjs-require-construct/input.ts new file mode 100644 index 000000000..81b4569be --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-require-construct/input.ts @@ -0,0 +1,2 @@ +const SentryCli = require("@sentry/cli"); +const cli = new SentryCli(null, { authToken: t }); diff --git a/packages/codemod-cli3to4/tests/cjs-require-ternary/expected.ts b/packages/codemod-cli3to4/tests/cjs-require-ternary/expected.ts new file mode 100644 index 000000000..e4ef38354 --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-require-ternary/expected.ts @@ -0,0 +1,3 @@ +const SentryCli = useCli ? require("sentry").default : null; +const cli = SentryCli({ token: t }); +await cli.release.finalize({ orgVersion: "1.0.0" }); diff --git a/packages/codemod-cli3to4/tests/cjs-require-ternary/input.ts b/packages/codemod-cli3to4/tests/cjs-require-ternary/input.ts new file mode 100644 index 000000000..e1f58c64c --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-require-ternary/input.ts @@ -0,0 +1,3 @@ +const SentryCli = useCli ? require("@sentry/cli") : null; +const cli = new SentryCli(null, { authToken: t }); +await cli.releases.finalize("1.0.0"); diff --git a/packages/codemod-cli3to4/tests/cjs-require/expected.ts b/packages/codemod-cli3to4/tests/cjs-require/expected.ts new file mode 100644 index 000000000..03dbe7db6 --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-require/expected.ts @@ -0,0 +1 @@ +const SentryCli = require('sentry').default; diff --git a/packages/codemod-cli3to4/tests/cjs-require/input.ts b/packages/codemod-cli3to4/tests/cjs-require/input.ts new file mode 100644 index 000000000..2f4d8c1a5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/cjs-require/input.ts @@ -0,0 +1 @@ +const SentryCli = require('@sentry/cli'); diff --git a/packages/codemod-cli3to4/tests/configfile-null-trailing-comma/expected.ts b/packages/codemod-cli3to4/tests/configfile-null-trailing-comma/expected.ts new file mode 100644 index 000000000..bb117e7c5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/configfile-null-trailing-comma/expected.ts @@ -0,0 +1,2 @@ +import SentryCli from "sentry"; +const cli = SentryCli(); diff --git a/packages/codemod-cli3to4/tests/configfile-null-trailing-comma/input.ts b/packages/codemod-cli3to4/tests/configfile-null-trailing-comma/input.ts new file mode 100644 index 000000000..7ec212209 --- /dev/null +++ b/packages/codemod-cli3to4/tests/configfile-null-trailing-comma/input.ts @@ -0,0 +1,5 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli( + "sentry.properties", + null, +); diff --git a/packages/codemod-cli3to4/tests/configfile-null/expected.ts b/packages/codemod-cli3to4/tests/configfile-null/expected.ts new file mode 100644 index 000000000..bb117e7c5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/configfile-null/expected.ts @@ -0,0 +1,2 @@ +import SentryCli from "sentry"; +const cli = SentryCli(); diff --git a/packages/codemod-cli3to4/tests/configfile-null/input.ts b/packages/codemod-cli3to4/tests/configfile-null/input.ts new file mode 100644 index 000000000..d823fade5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/configfile-null/input.ts @@ -0,0 +1,2 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli("sentry.properties", null); diff --git a/packages/codemod-cli3to4/tests/constructor-apikey/expected.ts b/packages/codemod-cli3to4/tests/constructor-apikey/expected.ts new file mode 100644 index 000000000..67957f9de --- /dev/null +++ b/packages/codemod-cli3to4/tests/constructor-apikey/expected.ts @@ -0,0 +1,3 @@ +import SentryCli from "sentry"; +// TODO(sentry-v4): verify these v3 options — they have no direct v4 SentryOptions equivalent: apiKey, silent (apiKey → rename to `token`; customHeader → move the "Name: value" string into the `headers` object; dsn → use SENTRY_DSN env; silent/vcsRemote were dropped) +const cli = SentryCli({ apiKey: process.env.KEY, silent: true }); diff --git a/packages/codemod-cli3to4/tests/constructor-apikey/input.ts b/packages/codemod-cli3to4/tests/constructor-apikey/input.ts new file mode 100644 index 000000000..3089e7270 --- /dev/null +++ b/packages/codemod-cli3to4/tests/constructor-apikey/input.ts @@ -0,0 +1,2 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(null, { apiKey: process.env.KEY, silent: true }); diff --git a/packages/codemod-cli3to4/tests/constructor/expected.ts b/packages/codemod-cli3to4/tests/constructor/expected.ts new file mode 100644 index 000000000..d1a41a4e5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/constructor/expected.ts @@ -0,0 +1,2 @@ +import SentryCli from "sentry"; +const cli = SentryCli({ token: process.env.T, org: "acme" }); diff --git a/packages/codemod-cli3to4/tests/constructor/input.ts b/packages/codemod-cli3to4/tests/constructor/input.ts new file mode 100644 index 000000000..1a207d096 --- /dev/null +++ b/packages/codemod-cli3to4/tests/constructor/input.ts @@ -0,0 +1,2 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(null, { authToken: process.env.T, org: "acme" }); diff --git a/packages/codemod-cli3to4/tests/custom-binding/expected.ts b/packages/codemod-cli3to4/tests/custom-binding/expected.ts new file mode 100644 index 000000000..1ac3dd380 --- /dev/null +++ b/packages/codemod-cli3to4/tests/custom-binding/expected.ts @@ -0,0 +1,3 @@ +import Sentry from "sentry"; +const cli = Sentry(); +export { Sentry }; diff --git a/packages/codemod-cli3to4/tests/custom-binding/input.ts b/packages/codemod-cli3to4/tests/custom-binding/input.ts new file mode 100644 index 000000000..8a4b7d3db --- /dev/null +++ b/packages/codemod-cli3to4/tests/custom-binding/input.ts @@ -0,0 +1,3 @@ +import Sentry from "@sentry/cli"; +const cli = new Sentry(); +export { Sentry }; diff --git a/packages/codemod-cli3to4/tests/esm-import/expected.ts b/packages/codemod-cli3to4/tests/esm-import/expected.ts new file mode 100644 index 000000000..9aa398084 --- /dev/null +++ b/packages/codemod-cli3to4/tests/esm-import/expected.ts @@ -0,0 +1 @@ +import SentryCli from "sentry"; diff --git a/packages/codemod-cli3to4/tests/esm-import/input.ts b/packages/codemod-cli3to4/tests/esm-import/input.ts new file mode 100644 index 000000000..778d0e898 --- /dev/null +++ b/packages/codemod-cli3to4/tests/esm-import/input.ts @@ -0,0 +1 @@ +import SentryCli from "@sentry/cli"; diff --git a/packages/codemod-cli3to4/tests/execute/expected.ts b/packages/codemod-cli3to4/tests/execute/expected.ts new file mode 100644 index 000000000..5fa4e9aa5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/execute/expected.ts @@ -0,0 +1,4 @@ +import SentryCli from "sentry"; +const cli = SentryCli(); +// TODO(sentry-v4): run(...) passes raw CLI args verbatim; remap v3 command names to v4 (releases→release, new→create, login→auth login, …) and verify flags. NOTE: v3's second `execute` arg (`live`) is dropped — v4 `run()` always captures and returns output (v3 `live:false` behavior); if you passed `live:true`/`'rejectOnError'` for streamed stdio, adjust accordingly +await cli.run("releases", "new", version); diff --git a/packages/codemod-cli3to4/tests/execute/input.ts b/packages/codemod-cli3to4/tests/execute/input.ts new file mode 100644 index 000000000..641a2a348 --- /dev/null +++ b/packages/codemod-cli3to4/tests/execute/input.ts @@ -0,0 +1,3 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(); +await cli.execute(["releases", "new", version], true); diff --git a/packages/codemod-cli3to4/tests/headers-option/expected.ts b/packages/codemod-cli3to4/tests/headers-option/expected.ts new file mode 100644 index 000000000..4409cb19a --- /dev/null +++ b/packages/codemod-cli3to4/tests/headers-option/expected.ts @@ -0,0 +1,2 @@ +import SentryCli from "sentry"; +const cli = SentryCli({ token: t, headers: { "X-Proxy": "1" } }); diff --git a/packages/codemod-cli3to4/tests/headers-option/input.ts b/packages/codemod-cli3to4/tests/headers-option/input.ts new file mode 100644 index 000000000..2aed6f093 --- /dev/null +++ b/packages/codemod-cli3to4/tests/headers-option/input.ts @@ -0,0 +1,2 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(null, { authToken: t, headers: { "X-Proxy": "1" } }); diff --git a/packages/codemod-cli3to4/tests/indented-todo/expected.ts b/packages/codemod-cli3to4/tests/indented-todo/expected.ts new file mode 100644 index 000000000..fabe617b2 --- /dev/null +++ b/packages/codemod-cli3to4/tests/indented-todo/expected.ts @@ -0,0 +1,6 @@ +import SentryCli from "sentry"; +async function upload() { + const cli = SentryCli(); + // TODO(sentry-v4): verify set-commits options: v3 `repo`/`commit`/`previousCommit` become one `commit: "REPO@PREV..SHA"` string; `ignoreMissing`/`ignoreEmpty` were dropped; `auto` is kept + await cli.release["set-commits"]({ orgVersion: "1.0.0", auto: true }); +} diff --git a/packages/codemod-cli3to4/tests/indented-todo/input.ts b/packages/codemod-cli3to4/tests/indented-todo/input.ts new file mode 100644 index 000000000..acfa47015 --- /dev/null +++ b/packages/codemod-cli3to4/tests/indented-todo/input.ts @@ -0,0 +1,5 @@ +import SentryCli from "@sentry/cli"; +async function upload() { + const cli = new SentryCli(); + await cli.releases.setCommits("1.0.0", { auto: true }); +} diff --git a/packages/codemod-cli3to4/tests/inject/expected.ts b/packages/codemod-cli3to4/tests/inject/expected.ts new file mode 100644 index 000000000..1d8ac4b6f --- /dev/null +++ b/packages/codemod-cli3to4/tests/inject/expected.ts @@ -0,0 +1,4 @@ +import { createSentrySDK as SentryCli } from "sentry"; +const cli = SentryCli(); +// TODO(sentry-v4): verify inject options: v4 takes one `directory` (v3 `paths` was an array — run once per directory), and `ignore`/`ext` are comma-separated strings; `ignoreFile`/`dryRun` are kept +await cli.sourcemap.inject({ paths: ["./dist"], dryRun: true }); diff --git a/packages/codemod-cli3to4/tests/inject/input.ts b/packages/codemod-cli3to4/tests/inject/input.ts new file mode 100644 index 000000000..ae38e71cc --- /dev/null +++ b/packages/codemod-cli3to4/tests/inject/input.ts @@ -0,0 +1,3 @@ +import { SentryCli } from "@sentry/cli"; +const cli = new SentryCli(); +await cli.sourceMaps.inject({ paths: ["./dist"], dryRun: true }); diff --git a/packages/codemod-cli3to4/tests/lone-null/expected.ts b/packages/codemod-cli3to4/tests/lone-null/expected.ts new file mode 100644 index 000000000..bb117e7c5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/lone-null/expected.ts @@ -0,0 +1,2 @@ +import SentryCli from "sentry"; +const cli = SentryCli(); diff --git a/packages/codemod-cli3to4/tests/lone-null/input.ts b/packages/codemod-cli3to4/tests/lone-null/input.ts new file mode 100644 index 000000000..76eabf069 --- /dev/null +++ b/packages/codemod-cli3to4/tests/lone-null/input.ts @@ -0,0 +1,2 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(null); diff --git a/packages/codemod-cli3to4/tests/named-import-alias/expected.ts b/packages/codemod-cli3to4/tests/named-import-alias/expected.ts new file mode 100644 index 000000000..9d73a7129 --- /dev/null +++ b/packages/codemod-cli3to4/tests/named-import-alias/expected.ts @@ -0,0 +1,5 @@ +// TODO(sentry-v4): v4 has no `SentryCliNewDeployOptions` export; use the parameter types of the v4 SDK methods instead +import { createSentrySDK as Cli, type SentryOptions as SentryCliOptions, SentryCliNewDeployOptions } from "sentry"; +const opts: SentryCliOptions = { authToken: "t" }; +// TODO(sentry-v4): v4 renamed the `authToken` option to `token` — update it inside the options object passed here +const cli = Cli(opts); diff --git a/packages/codemod-cli3to4/tests/named-import-alias/input.ts b/packages/codemod-cli3to4/tests/named-import-alias/input.ts new file mode 100644 index 000000000..f3a2d453a --- /dev/null +++ b/packages/codemod-cli3to4/tests/named-import-alias/input.ts @@ -0,0 +1,3 @@ +import { SentryCli as Cli, type SentryCliOptions, SentryCliNewDeployOptions } from "@sentry/cli"; +const opts: SentryCliOptions = { authToken: "t" }; +const cli = new Cli(null, opts); diff --git a/packages/codemod-cli3to4/tests/named-import/expected.ts b/packages/codemod-cli3to4/tests/named-import/expected.ts new file mode 100644 index 000000000..1719eb937 --- /dev/null +++ b/packages/codemod-cli3to4/tests/named-import/expected.ts @@ -0,0 +1,4 @@ +import { createSentrySDK as SentryCli } from "sentry"; +const cli = SentryCli({ token: process.env.T, org: "acme" }); +await cli.release.create({ orgVersion: "1.0.0" }); +await cli.release.finalize({ orgVersion: "1.0.0" }); diff --git a/packages/codemod-cli3to4/tests/named-import/input.ts b/packages/codemod-cli3to4/tests/named-import/input.ts new file mode 100644 index 000000000..d63e58d8d --- /dev/null +++ b/packages/codemod-cli3to4/tests/named-import/input.ts @@ -0,0 +1,4 @@ +import { SentryCli } from "@sentry/cli"; +const cli = new SentryCli(null, { authToken: process.env.T, org: "acme" }); +await cli.releases.new("1.0.0"); +await cli.releases.finalize("1.0.0"); diff --git a/packages/codemod-cli3to4/tests/namespace-default/expected.ts b/packages/codemod-cli3to4/tests/namespace-default/expected.ts new file mode 100644 index 000000000..daac9d1b9 --- /dev/null +++ b/packages/codemod-cli3to4/tests/namespace-default/expected.ts @@ -0,0 +1,4 @@ +import * as Sentry from "sentry"; +const cli = Sentry.default({ token: process.env.T }); +// TODO(sentry-v4): run(...) passes raw CLI args verbatim; remap v3 command names to v4 (releases→release, new→create, login→auth login, …) and verify flags. NOTE: v3's second `execute` arg (`live`) is dropped — v4 `run()` always captures and returns output (v3 `live:false` behavior); if you passed `live:true`/`'rejectOnError'` for streamed stdio, adjust accordingly +cli.run("releases", "list"); diff --git a/packages/codemod-cli3to4/tests/namespace-default/input.ts b/packages/codemod-cli3to4/tests/namespace-default/input.ts new file mode 100644 index 000000000..7121fb83b --- /dev/null +++ b/packages/codemod-cli3to4/tests/namespace-default/input.ts @@ -0,0 +1,3 @@ +import * as Sentry from "@sentry/cli"; +const cli = new Sentry.default({ authToken: process.env.T }); +cli.execute(["releases", "list"]); diff --git a/packages/codemod-cli3to4/tests/namespace-import/expected.ts b/packages/codemod-cli3to4/tests/namespace-import/expected.ts new file mode 100644 index 000000000..01112e761 --- /dev/null +++ b/packages/codemod-cli3to4/tests/namespace-import/expected.ts @@ -0,0 +1 @@ +import * as Sentry from "sentry"; diff --git a/packages/codemod-cli3to4/tests/namespace-import/input.ts b/packages/codemod-cli3to4/tests/namespace-import/input.ts new file mode 100644 index 000000000..b3c21dbd4 --- /dev/null +++ b/packages/codemod-cli3to4/tests/namespace-import/input.ts @@ -0,0 +1 @@ +import * as Sentry from "@sentry/cli"; diff --git a/packages/codemod-cli3to4/tests/namespace-named/expected.ts b/packages/codemod-cli3to4/tests/namespace-named/expected.ts new file mode 100644 index 000000000..140ad5321 --- /dev/null +++ b/packages/codemod-cli3to4/tests/namespace-named/expected.ts @@ -0,0 +1,3 @@ +import * as Sentry from "sentry"; +const cli = Sentry.createSentrySDK(); +await cli.release.deploys({ orgVersion: "1.0.0" }); diff --git a/packages/codemod-cli3to4/tests/namespace-named/input.ts b/packages/codemod-cli3to4/tests/namespace-named/input.ts new file mode 100644 index 000000000..1b3dfd30b --- /dev/null +++ b/packages/codemod-cli3to4/tests/namespace-named/input.ts @@ -0,0 +1,3 @@ +import * as Sentry from "@sentry/cli"; +const cli = new Sentry.SentryCli(); +await cli.releases.listDeploys("1.0.0"); diff --git a/packages/codemod-cli3to4/tests/new-deploy-shorthand/expected.ts b/packages/codemod-cli3to4/tests/new-deploy-shorthand/expected.ts new file mode 100644 index 000000000..d896abc4a --- /dev/null +++ b/packages/codemod-cli3to4/tests/new-deploy-shorthand/expected.ts @@ -0,0 +1,7 @@ +import { createSentrySDK as SentryCli } from "sentry"; +const cli = SentryCli(); +const env = "prod"; +// TODO(sentry-v4): verify deploy options: v3 `env` is v4 `environment`; `started`/`finished` are ISO 8601 strings and `time` is a string of seconds (v3 took numbers); `projects` was dropped +await cli.release.deploy({ orgVersion: "1.0.0", environment: env, name: "Deploy #1" }); +// TODO(sentry-v4): verify deploy options: v3 `env` is v4 `environment`; `started`/`finished` are ISO 8601 strings and `time` is a string of seconds (v3 took numbers); `projects` was dropped +await cli.release.deploy({ orgVersion: "1.0.0", ...deployOpts }); diff --git a/packages/codemod-cli3to4/tests/new-deploy-shorthand/input.ts b/packages/codemod-cli3to4/tests/new-deploy-shorthand/input.ts new file mode 100644 index 000000000..5cfe6f4e2 --- /dev/null +++ b/packages/codemod-cli3to4/tests/new-deploy-shorthand/input.ts @@ -0,0 +1,5 @@ +import { SentryCli } from "@sentry/cli"; +const cli = new SentryCli(); +const env = "prod"; +await cli.releases.newDeploy("1.0.0", { env, name: "Deploy #1" }); +await cli.releases.newDeploy("1.0.0", deployOpts); diff --git a/packages/codemod-cli3to4/tests/new-deploy/expected.ts b/packages/codemod-cli3to4/tests/new-deploy/expected.ts new file mode 100644 index 000000000..b06a0a383 --- /dev/null +++ b/packages/codemod-cli3to4/tests/new-deploy/expected.ts @@ -0,0 +1,4 @@ +import SentryCli from "sentry"; +const cli = SentryCli(); +// TODO(sentry-v4): verify deploy options: v3 `env` is v4 `environment`; `started`/`finished` are ISO 8601 strings and `time` is a string of seconds (v3 took numbers); `projects` was dropped +await cli.release.deploy({ orgVersion: "1.0.0", environment: "prod", url: "https://x" }); diff --git a/packages/codemod-cli3to4/tests/new-deploy/input.ts b/packages/codemod-cli3to4/tests/new-deploy/input.ts new file mode 100644 index 000000000..f25b06267 --- /dev/null +++ b/packages/codemod-cli3to4/tests/new-deploy/input.ts @@ -0,0 +1,3 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(); +await cli.releases.newDeploy("1.0.0", { env: "prod", url: "https://x" }); diff --git a/packages/codemod-cli3to4/tests/new-projects-dynamic/expected.ts b/packages/codemod-cli3to4/tests/new-projects-dynamic/expected.ts new file mode 100644 index 000000000..2b9ff2252 --- /dev/null +++ b/packages/codemod-cli3to4/tests/new-projects-dynamic/expected.ts @@ -0,0 +1,10 @@ +import { createSentrySDK as SentryCli } from "sentry"; +const cli = SentryCli(); +const projects = ["web", process.env.PROJECT!]; +await cli.release.create({ orgVersion: "1.0.0", project: projects.join(",") }); +await cli.release.create({ orgVersion: "1.0.1", project: [...projects, "api"].join(",") }); +await cli.release.create({ orgVersion: "1.0.2", project: newOpts.projects?.join(",") }); +// TODO(sentry-v4): map v3 `projects` (array) to v4 `project` (comma-separated string) by hand +await cli.release.create({ orgVersion: "1.0.3" }); +await cli.release.create({ orgVersion: "1.0.4", project: (ci ? ciProjects : ["web"]).join(",") }); +await cli.release.create({ orgVersion: "1.0.5", project: (ci ? ciOpts : localOpts).projects?.join(",") }); diff --git a/packages/codemod-cli3to4/tests/new-projects-dynamic/input.ts b/packages/codemod-cli3to4/tests/new-projects-dynamic/input.ts new file mode 100644 index 000000000..3f0773119 --- /dev/null +++ b/packages/codemod-cli3to4/tests/new-projects-dynamic/input.ts @@ -0,0 +1,9 @@ +import { SentryCli } from "@sentry/cli"; +const cli = new SentryCli(); +const projects = ["web", process.env.PROJECT!]; +await cli.releases.new("1.0.0", { projects }); +await cli.releases.new("1.0.1", { projects: [...projects, "api"] }); +await cli.releases.new("1.0.2", newOpts); +await cli.releases.new("1.0.3", { projects, ...extra }); +await cli.releases.new("1.0.4", { projects: ci ? ciProjects : ["web"] }); +await cli.releases.new("1.0.5", ci ? ciOpts : localOpts); diff --git a/packages/codemod-cli3to4/tests/new-projects/expected.ts b/packages/codemod-cli3to4/tests/new-projects/expected.ts new file mode 100644 index 000000000..17fdfdbe0 --- /dev/null +++ b/packages/codemod-cli3to4/tests/new-projects/expected.ts @@ -0,0 +1,3 @@ +import { createSentrySDK as SentryCli } from "sentry"; +const cli = SentryCli(); +await cli.release.create({ orgVersion: "1.0.0", project: "web,api" }); diff --git a/packages/codemod-cli3to4/tests/new-projects/input.ts b/packages/codemod-cli3to4/tests/new-projects/input.ts new file mode 100644 index 000000000..49963c5ed --- /dev/null +++ b/packages/codemod-cli3to4/tests/new-projects/input.ts @@ -0,0 +1,3 @@ +import { SentryCli } from "@sentry/cli"; +const cli = new SentryCli(); +await cli.releases.new("1.0.0", { projects: ["web", "api"] }); diff --git a/packages/codemod-cli3to4/tests/null-trailing-comma/expected.ts b/packages/codemod-cli3to4/tests/null-trailing-comma/expected.ts new file mode 100644 index 000000000..bb117e7c5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/null-trailing-comma/expected.ts @@ -0,0 +1,2 @@ +import SentryCli from "sentry"; +const cli = SentryCli(); diff --git a/packages/codemod-cli3to4/tests/null-trailing-comma/input.ts b/packages/codemod-cli3to4/tests/null-trailing-comma/input.ts new file mode 100644 index 000000000..d8aed4e2a --- /dev/null +++ b/packages/codemod-cli3to4/tests/null-trailing-comma/input.ts @@ -0,0 +1,4 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli( + null, +); diff --git a/packages/codemod-cli3to4/tests/release-flow/expected.ts b/packages/codemod-cli3to4/tests/release-flow/expected.ts new file mode 100644 index 000000000..928ca7246 --- /dev/null +++ b/packages/codemod-cli3to4/tests/release-flow/expected.ts @@ -0,0 +1,5 @@ +import SentryCli from "sentry"; +const cli = SentryCli({ token: t }); +await cli.release.create({ orgVersion: "1.0.0" }); +await cli.release.finalize({ orgVersion: "1.0.0" }); +const v = await cli.release["propose-version"](); diff --git a/packages/codemod-cli3to4/tests/release-flow/input.ts b/packages/codemod-cli3to4/tests/release-flow/input.ts new file mode 100644 index 000000000..ea1a875e8 --- /dev/null +++ b/packages/codemod-cli3to4/tests/release-flow/input.ts @@ -0,0 +1,5 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(null, { authToken: t }); +await cli.releases.new("1.0.0"); +await cli.releases.finalize("1.0.0"); +const v = await cli.releases.proposeVersion(); diff --git a/packages/codemod-cli3to4/tests/require-default/expected.ts b/packages/codemod-cli3to4/tests/require-default/expected.ts new file mode 100644 index 000000000..d6bcdeabe --- /dev/null +++ b/packages/codemod-cli3to4/tests/require-default/expected.ts @@ -0,0 +1,4 @@ +const SentryCli = require("sentry").default; +const cli = SentryCli({ token: process.env.T }); +// TODO(sentry-v4): run(...) passes raw CLI args verbatim; remap v3 command names to v4 (releases→release, new→create, login→auth login, …) and verify flags. NOTE: v3's second `execute` arg (`live`) is dropped — v4 `run()` always captures and returns output (v3 `live:false` behavior); if you passed `live:true`/`'rejectOnError'` for streamed stdio, adjust accordingly +cli.run("releases", "new", "1.0.0"); diff --git a/packages/codemod-cli3to4/tests/require-default/input.ts b/packages/codemod-cli3to4/tests/require-default/input.ts new file mode 100644 index 000000000..86d51ccb6 --- /dev/null +++ b/packages/codemod-cli3to4/tests/require-default/input.ts @@ -0,0 +1,3 @@ +const SentryCli = require("@sentry/cli").default; +const cli = new SentryCli({ authToken: process.env.T }); +cli.execute(["releases", "new", "1.0.0"], false); diff --git a/packages/codemod-cli3to4/tests/set-commits-comment/expected.ts b/packages/codemod-cli3to4/tests/set-commits-comment/expected.ts new file mode 100644 index 000000000..eea8b71a5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/set-commits-comment/expected.ts @@ -0,0 +1,6 @@ +import SentryCli from "sentry"; +const cli = SentryCli(); +// TODO(sentry-v4): verify set-commits options: v3 `repo`/`commit`/`previousCommit` become one `commit: "REPO@PREV..SHA"` string; `ignoreMissing`/`ignoreEmpty` were dropped; `auto` is kept +await cli.release["set-commits"]({ orgVersion: "1.0.0", + auto: true, // detect from git +}); diff --git a/packages/codemod-cli3to4/tests/set-commits-comment/input.ts b/packages/codemod-cli3to4/tests/set-commits-comment/input.ts new file mode 100644 index 000000000..e637c6e70 --- /dev/null +++ b/packages/codemod-cli3to4/tests/set-commits-comment/input.ts @@ -0,0 +1,5 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(); +await cli.releases.setCommits("1.0.0", { + auto: true, // detect from git +}); diff --git a/packages/codemod-cli3to4/tests/set-commits/expected.ts b/packages/codemod-cli3to4/tests/set-commits/expected.ts new file mode 100644 index 000000000..b7e9222d0 --- /dev/null +++ b/packages/codemod-cli3to4/tests/set-commits/expected.ts @@ -0,0 +1,4 @@ +import SentryCli from "sentry"; +const cli = SentryCli(); +// TODO(sentry-v4): verify set-commits options: v3 `repo`/`commit`/`previousCommit` become one `commit: "REPO@PREV..SHA"` string; `ignoreMissing`/`ignoreEmpty` were dropped; `auto` is kept +await cli.release["set-commits"]({ orgVersion: "1.0.0", auto: true }); diff --git a/packages/codemod-cli3to4/tests/set-commits/input.ts b/packages/codemod-cli3to4/tests/set-commits/input.ts new file mode 100644 index 000000000..4ab2034c5 --- /dev/null +++ b/packages/codemod-cli3to4/tests/set-commits/input.ts @@ -0,0 +1,3 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(); +await cli.releases.setCommits("1.0.0", { auto: true }); diff --git a/packages/codemod-cli3to4/tests/shorthand/expected.ts b/packages/codemod-cli3to4/tests/shorthand/expected.ts new file mode 100644 index 000000000..35734fb7f --- /dev/null +++ b/packages/codemod-cli3to4/tests/shorthand/expected.ts @@ -0,0 +1,2 @@ +import SentryCli from "sentry"; +const cli = SentryCli({ token: authToken }); diff --git a/packages/codemod-cli3to4/tests/shorthand/input.ts b/packages/codemod-cli3to4/tests/shorthand/input.ts new file mode 100644 index 000000000..d3df3bc42 --- /dev/null +++ b/packages/codemod-cli3to4/tests/shorthand/input.ts @@ -0,0 +1,2 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(null, { authToken }); diff --git a/packages/codemod-cli3to4/tests/single-var-ambiguous/expected.ts b/packages/codemod-cli3to4/tests/single-var-ambiguous/expected.ts new file mode 100644 index 000000000..f5cc639a4 --- /dev/null +++ b/packages/codemod-cli3to4/tests/single-var-ambiguous/expected.ts @@ -0,0 +1,3 @@ +import SentryCli from "sentry"; +// TODO(sentry-v4): verify this argument: v3's first constructor param was a configFile path (removed in v4); v4 takes an options object. Drop it if it's a config path, or map authToken→token if it's options +const cli = SentryCli(myOptions); diff --git a/packages/codemod-cli3to4/tests/single-var-ambiguous/input.ts b/packages/codemod-cli3to4/tests/single-var-ambiguous/input.ts new file mode 100644 index 000000000..bf6888448 --- /dev/null +++ b/packages/codemod-cli3to4/tests/single-var-ambiguous/input.ts @@ -0,0 +1,2 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(myOptions); diff --git a/packages/codemod-cli3to4/tests/static-helpers/expected.ts b/packages/codemod-cli3to4/tests/static-helpers/expected.ts new file mode 100644 index 000000000..e5ebb0848 --- /dev/null +++ b/packages/codemod-cli3to4/tests/static-helpers/expected.ts @@ -0,0 +1,5 @@ +import { createSentrySDK as SentryCli } from "sentry"; +import { execFileSync } from "node:child_process"; +console.log(SentryCli.getVersion()); +// TODO(sentry-v4): v4 `getPath()` is a Node.js script, not a native binary: spawn it as `execFile(process.execPath, [getPath(), ...args])`, or call the SDK methods instead +execFileSync(SentryCli.getPath(), ["--version"]); diff --git a/packages/codemod-cli3to4/tests/static-helpers/input.ts b/packages/codemod-cli3to4/tests/static-helpers/input.ts new file mode 100644 index 000000000..ad1123e24 --- /dev/null +++ b/packages/codemod-cli3to4/tests/static-helpers/input.ts @@ -0,0 +1,4 @@ +import { SentryCli } from "@sentry/cli"; +import { execFileSync } from "node:child_process"; +console.log(SentryCli.getVersion()); +execFileSync(SentryCli.getPath(), ["--version"]); diff --git a/packages/codemod-cli3to4/tests/type-only-import/expected.ts b/packages/codemod-cli3to4/tests/type-only-import/expected.ts new file mode 100644 index 000000000..44a884299 --- /dev/null +++ b/packages/codemod-cli3to4/tests/type-only-import/expected.ts @@ -0,0 +1,5 @@ +import type { SentryOptions as SentryCliOptions } from "sentry"; + +export function makeOptions(org: string): SentryCliOptions { + return { org }; +} diff --git a/packages/codemod-cli3to4/tests/type-only-import/input.ts b/packages/codemod-cli3to4/tests/type-only-import/input.ts new file mode 100644 index 000000000..436927ee0 --- /dev/null +++ b/packages/codemod-cli3to4/tests/type-only-import/input.ts @@ -0,0 +1,5 @@ +import type { SentryCliOptions } from "@sentry/cli"; + +export function makeOptions(org: string): SentryCliOptions { + return { org }; +} diff --git a/packages/codemod-cli3to4/tests/type-only-specifier/expected.ts b/packages/codemod-cli3to4/tests/type-only-specifier/expected.ts new file mode 100644 index 000000000..b76ad596a --- /dev/null +++ b/packages/codemod-cli3to4/tests/type-only-specifier/expected.ts @@ -0,0 +1,3 @@ +import { type SentryOptions as SentryCliOptions } from "sentry"; + +export const options: SentryCliOptions = { org: "acme" }; diff --git a/packages/codemod-cli3to4/tests/type-only-specifier/input.ts b/packages/codemod-cli3to4/tests/type-only-specifier/input.ts new file mode 100644 index 000000000..1e674dc16 --- /dev/null +++ b/packages/codemod-cli3to4/tests/type-only-specifier/input.ts @@ -0,0 +1,3 @@ +import { type SentryCliOptions } from "@sentry/cli"; + +export const options: SentryCliOptions = { org: "acme" }; diff --git a/packages/codemod-cli3to4/tests/unrelated-execute/expected.ts b/packages/codemod-cli3to4/tests/unrelated-execute/expected.ts new file mode 100644 index 000000000..8af8ecb46 --- /dev/null +++ b/packages/codemod-cli3to4/tests/unrelated-execute/expected.ts @@ -0,0 +1,2 @@ +const db = getDb(); +await db.execute("SELECT 1"); diff --git a/packages/codemod-cli3to4/tests/unrelated-execute/input.ts b/packages/codemod-cli3to4/tests/unrelated-execute/input.ts new file mode 100644 index 000000000..8af8ecb46 --- /dev/null +++ b/packages/codemod-cli3to4/tests/unrelated-execute/input.ts @@ -0,0 +1,2 @@ +const db = getDb(); +await db.execute("SELECT 1"); diff --git a/packages/codemod-cli3to4/tests/unrelated-no-import/expected.ts b/packages/codemod-cli3to4/tests/unrelated-no-import/expected.ts new file mode 100644 index 000000000..62e19e1a8 --- /dev/null +++ b/packages/codemod-cli3to4/tests/unrelated-no-import/expected.ts @@ -0,0 +1,2 @@ +const cli = new SentryCli(); +cli.releases.new("1.0.0"); diff --git a/packages/codemod-cli3to4/tests/unrelated-no-import/input.ts b/packages/codemod-cli3to4/tests/unrelated-no-import/input.ts new file mode 100644 index 000000000..62e19e1a8 --- /dev/null +++ b/packages/codemod-cli3to4/tests/unrelated-no-import/input.ts @@ -0,0 +1,2 @@ +const cli = new SentryCli(); +cli.releases.new("1.0.0"); diff --git a/packages/codemod-cli3to4/tests/upload-sourcemaps/expected.ts b/packages/codemod-cli3to4/tests/upload-sourcemaps/expected.ts new file mode 100644 index 000000000..5af78239c --- /dev/null +++ b/packages/codemod-cli3to4/tests/upload-sourcemaps/expected.ts @@ -0,0 +1,4 @@ +import SentryCli from "sentry"; +const cli = SentryCli(); +// TODO(sentry-v4): sourcemaps are debug-ID-first in v4: set the `directory` option to your bundle output dir (v3 `include` was a path array — run one upload per directory) and review the remaining options +await cli.sourcemap.upload({ release: "1.0.0" }); diff --git a/packages/codemod-cli3to4/tests/upload-sourcemaps/input.ts b/packages/codemod-cli3to4/tests/upload-sourcemaps/input.ts new file mode 100644 index 000000000..543a63113 --- /dev/null +++ b/packages/codemod-cli3to4/tests/upload-sourcemaps/input.ts @@ -0,0 +1,3 @@ +import SentryCli from "@sentry/cli"; +const cli = new SentryCli(); +await cli.releases.uploadSourceMaps("1.0.0", { include: ["./dist"] }); diff --git a/packages/codemod-cli3to4/tsconfig.json b/packages/codemod-cli3to4/tsconfig.json new file mode 100644 index 000000000..9c066b590 --- /dev/null +++ b/packages/codemod-cli3to4/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "skipLibCheck": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["@codemod.com/jssg-types"], + "allowImportingTsExtensions": true, + "noEmit": true, + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": ["tests"] +} diff --git a/packages/codemod-cli3to4/workflow.yaml b/packages/codemod-cli3to4/workflow.yaml new file mode 100644 index 000000000..d53f85b3e --- /dev/null +++ b/packages/codemod-cli3to4/workflow.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Scan JavaScript and TypeScript files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + base_path: "." + language: "typescript" + include: + - "**/*.{js,mjs,cjs,ts,mts,cts}" + exclude: &exclude + - "**/node_modules/**" + - "**/dist/**" + - "**/build/**" + # JSX needs the tsx grammar; the typescript grammar can't parse it. + - name: "Scan JSX and TSX files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + base_path: "." + language: "tsx" + include: + - "**/*.{jsx,tsx}" + exclude: *exclude diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f33845c5..d8bf3f05b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -681,6 +681,15 @@ importers: specifier: 3.4.0 version: 3.4.0 + packages/codemod-cli3to4: + devDependencies: + '@codemod.com/jssg-types': + specifier: 1.6.3 + version: 1.6.3 + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/logger: devDependencies: '@types/node': @@ -1808,6 +1817,9 @@ packages: '@cloudflare/workers-types@4.20260405.1': resolution: {integrity: sha512-PokTmySa+D6MY01R1UfYH48korsN462NK/fl3aw47Hg7XuLuSo/RTpjT0vtWaJhJoFY5tHGOBBIbDcIc8wltLg==} + '@codemod.com/jssg-types@1.6.3': + resolution: {integrity: sha512-b5829ixO5TdGL5xg5YcL9YfiLC3WwFZU+8Zx78NpsHlJ1iySbqfA0g0psqNXv8ss/r7hdpy2KiBXm3GE6bN5bQ==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -10290,6 +10302,8 @@ snapshots: '@cloudflare/workers-types@4.20260405.1': {} + '@codemod.com/jssg-types@1.6.3': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 diff --git a/scripts/codemod-version-changed.mjs b/scripts/codemod-version-changed.mjs new file mode 100644 index 000000000..1115bf4d8 --- /dev/null +++ b/scripts/codemod-version-changed.mjs @@ -0,0 +1,56 @@ +import { execFileSync } from "node:child_process"; +import { appendFileSync, readFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; + +/** The codemod manifest whose `version` decides whether to publish. */ +export const CODEMOD_MANIFEST = "packages/codemod-cli3to4/codemod.yaml"; + +/** Read the top-level `version` from a codemod.yaml, or null when absent. */ +export function codemodVersion(manifest) { + const match = /^version:\s*["']?([^"'\s#]+)["']?\s*(?:#.*)?$/m.exec(manifest); + return match ? match[1] : null; +} + +/** + * Whether a push changed the manifest version. A push with no usable + * previous commit (a new branch, or a manifest that didn't exist yet) counts + * as a change, so the first version publishes. + */ +export function versionChanged(previousManifest, currentManifest) { + const current = codemodVersion(currentManifest); + if (!current) { + throw new Error(`${CODEMOD_MANIFEST} has no version`); + } + return ( + previousManifest === null || codemodVersion(previousManifest) !== current + ); +} + +/** Return the manifest at `sha`, or null when the commit or file is missing. */ +function manifestAt(sha) { + if (!sha || /^0+$/.test(sha)) { + return null; + } + try { + return execFileSync("git", ["show", `${sha}:${CODEMOD_MANIFEST}`], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + } catch { + return null; + } +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(process.argv[1]).href +) { + const changed = versionChanged( + manifestAt(process.env.BEFORE_SHA), + readFileSync(CODEMOD_MANIFEST, "utf8"), + ); + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `changed=${changed}\n`); + } + console.log(`Codemod version changed: ${changed}`); +} diff --git a/scripts/codemod-version-changed.test.mjs b/scripts/codemod-version-changed.test.mjs new file mode 100644 index 000000000..a0a700ae4 --- /dev/null +++ b/scripts/codemod-version-changed.test.mjs @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { codemodVersion, versionChanged } from "./codemod-version-changed.mjs"; + +const manifest = (version) => + `schema_version: "1.0"\n\nname: "@getsentry/cli3to4"\nversion: ${version}\n`; + +test("reads quoted, unquoted, and commented versions", () => { + assert.equal(codemodVersion(manifest('"0.1.0"')), "0.1.0"); + assert.equal(codemodVersion(manifest("0.2.0")), "0.2.0"); + assert.equal(codemodVersion(manifest("'1.0.0' # bump me")), "1.0.0"); + assert.equal(codemodVersion('schema_version: "1.0"\n'), null); +}); + +test("publishes only when the version changes", () => { + assert.equal(versionChanged(manifest('"0.1.0"'), manifest('"0.1.0"')), false); + assert.equal(versionChanged(manifest('"0.1.0"'), manifest('"0.2.0"')), true); +}); + +test("publishes the first version when there is no previous manifest", () => { + assert.equal(versionChanged(null, manifest('"0.1.0"')), true); +}); + +test("fails loudly when the manifest has no version", () => { + assert.throws(() => versionChanged(null, 'name: "x"\n')); +});