diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e28e4f3f5..b5263cbcd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## Unreleased + +### New Sentry Support Policy + +`sentry-cli` 3.0.0 and above only officially supports Sentry SaaS and Sentry self-hosted versions [25.11.1](https://github.com/getsentry/sentry/releases/tag/25.11.1) and higher. While many Sentry CLI features may, in practice, continue working with some older Sentry versions, continued support for Sentry versions older than 25.11.1 is not guaranteed. Changes which break support for Sentry versions below 25.11.1 may occur in minor or patch releases. + +### Breaking Changes + +- Removed all `sentry-cli files ...` and `sentry-cli releases files ...` subcommands ([#2956](https://github.com/getsentry/sentry-cli/pull/2956)). These commands provided functionality for managing release files, a feature that has been deprecated in Sentry. Users still using `sentry-cli files upload` to upload source maps should migrate to `sentry-cli sourcemaps upload`. +- Removed the `sentry-cli sourcemaps explain` command ([#2947](https://github.com/getsentry/sentry-cli/pull/2947)). The command had been deprecated for some time, since Sentry now has a better in-product debugging flow for source map problems via the "Unminify Code" button, which is displayed on any JavaScript issues which could not be unminified. +- Removed support for the legacy API key authentication method ([#2935](https://github.com/getsentry/sentry-cli/pull/2935)). Sentry CLI now only supports authenticating with Auth Tokens. If you are using API key authentication via any of the following methods, you need to generate and use an [Auth Token](https://docs.sentry.io/account/auth-tokens/), instead: + - `--api-key` CLI flag + - `SENTRY_API_KEY` environment variable + - `api_key` configuration file field + - `apiKey` option in the JavaScript API +- Removed the `upload-proguard` subcommand's `--app-id`, `--version`, `--version-code`, `--android-manifest`, and `--platform` arguments ([#2876](https://github.com/getsentry/sentry-cli/pull/2876), [#2940](https://github.com/getsentry/sentry-cli/pull/2940), [#2948](https://github.com/getsentry/sentry-cli/pull/2948)). Users using these arguments should stop using them, as they are unnecessary. The information passed to these arguments is no longer visible in Sentry. + +#### Node.js Wrapper Breakages + +The following changes only apply when using `sentry-cli` via the npm package [`@sentry/cli`](https://www.npmjs.com/package/@sentry/cli): + +- Removed the `apiKey` option from `SentryCliOptions` ([#2935](https://github.com/getsentry/sentry-cli/pull/2935)). If you are using `apiKey`, you need to generate and use an [Auth Token](https://docs.sentry.io/account/auth-tokens/) via the `authToken` option, instead. + +### Improvements + +- The `sentry-cli upload-proguard` command now uses chunked uploading by default ([#2918](https://github.com/getsentry/sentry-cli/pull/2918)). Users who previously set the `SENTRY_EXPERIMENTAL_PROGUARD_CHUNK_UPLOAD` environment variable to opt into this behavior no longer need to set the variable. + +### Fixes + +- Fixed misleading error message claiming the server doesn't support chunk uploading when the actual error was a non-existent organization ([#2930](https://github.com/getsentry/sentry-cli/pull/2930)). + +## 2.58.4 + +### Fixes + +- Use node directly in the postinstall script, instead of using `npm run` ([#3030](https://github.com/getsentry/sentry-cli/pull/3030)). This change ensures the postinstall script remains compatible with package managers other than `npm`. + ## 2.58.3 ### Improvements diff --git a/Cargo.lock b/Cargo.lock index 617b6d650b..22fb4c31b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2726,7 +2726,7 @@ dependencies = [ [[package]] name = "sentry-cli" -version = "2.58.3" +version = "2.58.4" dependencies = [ "anyhow", "anylog", diff --git a/Cargo.toml b/Cargo.toml index c99979b13d..a682fd89d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "sentry-cli" -version = "2.58.3" +version = "2.58.4" edition = "2021" rust-version = "1.91" diff --git a/README.md b/README.md index 344d826097..284608425a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,23 @@ Sentry CLI can be used for many tasks, including uploading debug symbols and sou Please refer to [Sentry CLI's documentation page](https://docs.sentry.io/cli/). +## Compatibility + +Sentry CLI officially supports [Sentry SaaS](https://sentry.io/) and [Sentry Self-Hosted](https://github.com/getsentry/self-hosted) versions 25.11.1 and above. + +### Self-Hosted Sentry + +Although some Sentry CLI features may work with versions of Sentry Self-Hosted prior to 25.11.1, we recommend users upgrade their self-hosted installations to a compatible version. + +For users who cannot upgrade their self-hosted installation, we recommend using the latest compatible Sentry CLI version, per the table below: + +| **Sentry Self-Hosted Version** | **Newest Compatible Sentry CLI Version** | +| ------------------------------ | --------------------------------------------------------------------- | +| ≥ 25.11.1 | [latest](https://github.com/getsentry/sentry-cli/releases/latest) | +| < 25.11.1 | [2.58.4](https://github.com/getsentry/sentry-cli/releases/tag/2.58.4) | + +Note that we can only provide support for officially-supported Sentry Self-Hosted versions. We will not backport fixes for older Sentry CLI versions, even if they should be compatible with your self-hosted version. + ## Compiling In case you want to compile this yourself, you need to install at minimum the diff --git a/apple-catalog-parsing/native/swift/AssetCatalogParser/Sources/AssetCatalogParser/AssetCatalogReader.swift b/apple-catalog-parsing/native/swift/AssetCatalogParser/Sources/AssetCatalogParser/AssetCatalogReader.swift index aa6014fb70..6d12ac7443 100644 --- a/apple-catalog-parsing/native/swift/AssetCatalogParser/Sources/AssetCatalogParser/AssetCatalogReader.swift +++ b/apple-catalog-parsing/native/swift/AssetCatalogParser/Sources/AssetCatalogParser/AssetCatalogReader.swift @@ -1,4 +1,5 @@ import CoreGraphics +import CryptoKit import Foundation import ImageIO import UniformTypeIdentifiers @@ -40,6 +41,7 @@ struct AssetCatalogEntry: Encodable { let type: AssetType? let idiom: String? let colorspace: String? + let contentHash: String? } enum Error: Swift.Error { @@ -112,7 +114,7 @@ enum AssetUtil { let (structuredThemeStore, assetKeys) = initializeCatalog(from: file) - var images: [String: (cgImage: CGImage, format: String)] = [:] + var cgImages: [String: (cgImage: CGImage, format: String)] = [:] // First pass: Build map of multisize sets and cache renditions for performance var multisizeSets: [MultisizeSetInfo] = [] @@ -216,6 +218,7 @@ enum AssetUtil { var width: Int? var height: Int? var unslicedImage: CGImage? + var contentHash: String? = nil if isMultisizeImageSet { continue @@ -223,10 +226,15 @@ enum AssetUtil { // Get image dimensions from regular rendition (width, height, unslicedImage) = resolveImageDimensions(rendition, isVector) - // Skip SVGs, but save images even if they don't have an extension (default to png) - if fileExtension != "svg", let unslicedImage = unslicedImage { + // Compute content hash for PDFs/SVGs without saving to disk + if fileExtension == "pdf" || fileExtension == "svg" { + // Hash PDFs/SVGs in-memory (Python can't access _srcData without parsing binary .car format) + contentHash = data.sha256Hash() + } + // Save images that can be converted to CGImage (excluding PDFs/SVGs) + else if let unslicedImage = unslicedImage { let format = fileExtension.isEmpty ? "png" : fileExtension - images[imageId] = (cgImage: unslicedImage, format: format) + cgImages[imageId] = (cgImage: unslicedImage, format: format) } } @@ -251,7 +259,8 @@ enum AssetUtil { filename: renditionTypeName, type: assetType, idiom: idiomToString(idiomValue), - colorspace: colorSpaceIDToString(colorSpaceID) + colorspace: colorSpaceIDToString(colorSpaceID), + contentHash: contentHash ) assets.append(asset) } @@ -266,7 +275,8 @@ enum AssetUtil { filename: nil, type: nil, idiom: nil, - colorspace: nil + colorspace: nil, + contentHash: nil )) let data = try! JSONEncoder().encode(assets) @@ -275,7 +285,7 @@ enum AssetUtil { .appendingPathComponent("Assets") .appendingPathExtension("json") try! data.write(to: url, options: []) - for (id, imageInfo) in images { + for (id, imageInfo) in cgImages { let format = imageInfo.format let cgImage = imageInfo.cgImage let fileURL = folder.appendingPathComponent(id).appendingPathExtension(format) @@ -460,6 +470,17 @@ enum AssetUtil { } } +private extension Data { + func sha256Hash() -> String { + if #available(macOS 10.15, *) { + let digest = SHA256.hash(data: self) + return digest.map { String(format: "%02x", $0) }.joined() + } + // Fallback for older macOS (shouldn't happen with version 13+ requirement) + return "" + } +} + private extension NSObject { func getUInt(forKey key: String) -> UInt? { if let result = perform(Selector(key)) { diff --git a/lib/helper.js b/lib/helper.js index 19028a5ddb..f2784b9989 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -302,9 +302,6 @@ async function execute(args, live, silent, configFile, config = {}) { if (config.authToken) { env.SENTRY_AUTH_TOKEN = config.authToken; } - if (config.apiKey) { - env.SENTRY_API_KEY = config.apiKey; - } if (config.dsn) { env.SENTRY_DSN = config.dsn; } diff --git a/lib/types.ts b/lib/types.ts index 4e703921ea..26d92214fb 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -16,13 +16,6 @@ export interface SentryCliOptions { * This value will update `SENTRY_AUTH_TOKEN` env variable. */ authToken?: string; - /** - * API key to authenticate any HTTP requests to Sentry (legacy authentication method). - * This value will update `SENTRY_API_KEY` env variable. - * @deprecated Use auth-token-based authentication via `authToken` instead. - * This option is scheduled for removal in the next major release. - */ - apiKey?: string; /** * Sentry DSN. * This value will update `SENTRY_DSN` env variable. @@ -68,7 +61,6 @@ export type SourceMapsPathDescriptor = Omit; + new (release: string, options?: { projects: string[] } | string[]): Promise; setCommits(release: string, options: SentryCliCommitsOptions): Promise; @@ -241,4 +233,3 @@ export interface SentryCliReleases { execute(args: string[], live: boolean | 'rejectOnError'): Promise; } - diff --git a/npm-binary-distributions/darwin/package.json b/npm-binary-distributions/darwin/package.json index 1db22057cc..05e118615d 100644 --- a/npm-binary-distributions/darwin/package.json +++ b/npm-binary-distributions/darwin/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli-darwin", - "version": "2.58.3", + "version": "2.58.4", "description": "The darwin distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "FSL-1.1-MIT", diff --git a/npm-binary-distributions/linux-arm/package.json b/npm-binary-distributions/linux-arm/package.json index 8dbd8888f7..976e002805 100644 --- a/npm-binary-distributions/linux-arm/package.json +++ b/npm-binary-distributions/linux-arm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli-linux-arm", - "version": "2.58.3", + "version": "2.58.4", "description": "The linux arm distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "FSL-1.1-MIT", diff --git a/npm-binary-distributions/linux-arm64/package.json b/npm-binary-distributions/linux-arm64/package.json index 68b55fdeaf..3c39d9f763 100644 --- a/npm-binary-distributions/linux-arm64/package.json +++ b/npm-binary-distributions/linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli-linux-arm64", - "version": "2.58.3", + "version": "2.58.4", "description": "The linux arm64 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "FSL-1.1-MIT", diff --git a/npm-binary-distributions/linux-i686/package.json b/npm-binary-distributions/linux-i686/package.json index 8aa459a6b6..e34919f39e 100644 --- a/npm-binary-distributions/linux-i686/package.json +++ b/npm-binary-distributions/linux-i686/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli-linux-i686", - "version": "2.58.3", + "version": "2.58.4", "description": "The linux x86 and ia32 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "FSL-1.1-MIT", diff --git a/npm-binary-distributions/linux-x64/package.json b/npm-binary-distributions/linux-x64/package.json index 4b93201845..663b8bdfcf 100644 --- a/npm-binary-distributions/linux-x64/package.json +++ b/npm-binary-distributions/linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli-linux-x64", - "version": "2.58.3", + "version": "2.58.4", "description": "The linux x64 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "FSL-1.1-MIT", diff --git a/npm-binary-distributions/win32-arm64/package.json b/npm-binary-distributions/win32-arm64/package.json index a744cdf484..413bac58a2 100644 --- a/npm-binary-distributions/win32-arm64/package.json +++ b/npm-binary-distributions/win32-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli-win32-arm64", - "version": "2.58.3", + "version": "2.58.4", "description": "The windows arm64 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "FSL-1.1-MIT", diff --git a/npm-binary-distributions/win32-i686/package.json b/npm-binary-distributions/win32-i686/package.json index 9e702ec288..ab7f6b3ac2 100644 --- a/npm-binary-distributions/win32-i686/package.json +++ b/npm-binary-distributions/win32-i686/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli-win32-i686", - "version": "2.58.3", + "version": "2.58.4", "description": "The windows x86 and ia32 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "FSL-1.1-MIT", diff --git a/npm-binary-distributions/win32-x64/package.json b/npm-binary-distributions/win32-x64/package.json index ebae3c65e8..164434fd9b 100644 --- a/npm-binary-distributions/win32-x64/package.json +++ b/npm-binary-distributions/win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli-win32-x64", - "version": "2.58.3", + "version": "2.58.4", "description": "The windows x64 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "FSL-1.1-MIT", diff --git a/package-lock.json b/package-lock.json index bed412ec14..81cf102f14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli", - "version": "2.58.3", + "version": "2.58.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -767,51 +767,51 @@ } }, "@sentry/cli-darwin": { - "version": "2.58.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.3.tgz", - "integrity": "sha512-4bnI5e07cVkG3DdTPpBJcoRjwHvf8GPCY/vBH6gGTrM+cXxGgk7jiZpkblq1ZvpW5H51mFjiO5qqMO7GNjstQA==", + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.4.tgz", + "integrity": "sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ==", "optional": true }, "@sentry/cli-linux-arm": { - "version": "2.58.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.3.tgz", - "integrity": "sha512-33mlrULHtxK5rjzO7YJDtXyOeMTpkZOgfcQqcjyxeDY91jSCXE0EhlBV6SGCKqvahmvh9RUgINLkQO+qc2OjDg==", + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.4.tgz", + "integrity": "sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA==", "optional": true }, "@sentry/cli-linux-arm64": { - "version": "2.58.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.3.tgz", - "integrity": "sha512-xpn94gpy8U+JwsY4Eamlx/gNXh5YiZTmA1XT73kOUBrLs+e+qwQh6gfnzGxx7wdX9x+CJooTw8rwP4unisGALQ==", + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.4.tgz", + "integrity": "sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig==", "optional": true }, "@sentry/cli-linux-i686": { - "version": "2.58.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.3.tgz", - "integrity": "sha512-Z4qWllBAPa0Z3nO3cvw9bTIfASxo5EYjnid7MvWB99TKUTdd2+YCIJ9rP9UadZqvOE9AekF+DHdh35j9E1urOQ==", + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.4.tgz", + "integrity": "sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA==", "optional": true }, "@sentry/cli-linux-x64": { - "version": "2.58.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.3.tgz", - "integrity": "sha512-i68RTLedYzxZ+wdh9JS8aeMCWbnJq4xYa3Rmib2lrQ9AvScBLJlFbafjYnZUy9PWiqc78PFJc5gLefPRDpIexw==", + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.4.tgz", + "integrity": "sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q==", "optional": true }, "@sentry/cli-win32-arm64": { - "version": "2.58.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.3.tgz", - "integrity": "sha512-en2uDEi+NLEVCfTvuivKxUSSe2jp3iKtSRFSONSRMc0p7O6SlGYflPZCVIGPXQWKSNYXdQINbefQND4d5bPzsQ==", + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.4.tgz", + "integrity": "sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w==", "optional": true }, "@sentry/cli-win32-i686": { - "version": "2.58.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.3.tgz", - "integrity": "sha512-dYWNuE+FtZXpT4aqW68/4ah2WAAz//QSQxxpuTBAQ+Wui26Mp30q5nJdYOzAckdSF8yVKjIHB0NLuIYZYHijyQ==", + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.4.tgz", + "integrity": "sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug==", "optional": true }, "@sentry/cli-win32-x64": { - "version": "2.58.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.3.tgz", - "integrity": "sha512-D1qkHE2Mkfu9w1zyZt8M/V+yqoycJFEtp4hCv/HtLhExNtZYrdjf8LHzR0tUvEBS4Y1aw4wbox+Wh8ToOhcXbQ==", + "version": "2.58.4", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.4.tgz", + "integrity": "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w==", "optional": true }, "@sinonjs/commons": { diff --git a/package.json b/package.json index 89c9bdd2d9..e31e9c0b60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli", - "version": "2.58.3", + "version": "2.58.4", "description": "A command line utility to work with Sentry. https://docs.sentry.io/hosted/learn/cli/", "repository": "git://github.com/getsentry/sentry-cli.git", "homepage": "https://docs.sentry.io/hosted/learn/cli/", @@ -32,19 +32,18 @@ "typescript": "5.8.3" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.58.3", - "@sentry/cli-linux-arm": "2.58.3", - "@sentry/cli-linux-arm64": "2.58.3", - "@sentry/cli-linux-i686": "2.58.3", - "@sentry/cli-linux-x64": "2.58.3", - "@sentry/cli-win32-i686": "2.58.3", - "@sentry/cli-win32-x64": "2.58.3", - "@sentry/cli-win32-arm64": "2.58.3" + "@sentry/cli-darwin": "2.58.4", + "@sentry/cli-linux-arm": "2.58.4", + "@sentry/cli-linux-arm64": "2.58.4", + "@sentry/cli-linux-i686": "2.58.4", + "@sentry/cli-linux-x64": "2.58.4", + "@sentry/cli-win32-i686": "2.58.4", + "@sentry/cli-win32-x64": "2.58.4", + "@sentry/cli-win32-arm64": "2.58.4" }, "scripts": { - "postinstall": "npm run install-cli", + "postinstall": "node ./scripts/install.js", "build": "tsc", - "install-cli": "node ./scripts/install.js", "prepack": "npm run build", "fix": "npm-run-all fix:eslint fix:prettier", "fix:eslint": "eslint --fix bin/* scripts/**/*.js lib/**/*.js", diff --git a/src/api/data_types/chunking/dif.rs b/src/api/data_types/chunking/dif.rs index 43a6977460..f8ddc20b24 100644 --- a/src/api/data_types/chunking/dif.rs +++ b/src/api/data_types/chunking/dif.rs @@ -52,21 +52,6 @@ pub struct ChunkedDifResponse { #[serde(transparent)] pub struct AssembleDifsRequest<'a>(HashMap>); -impl AssembleDifsRequest<'_> { - /// Strips the debug_id from all requests in the request. We need - /// to strip the debug_ids whenever the server does not support chunked - /// uploading of PDBs, to maintain backwards compatibility. The - /// calling code is responsible for calling this function when needed. - /// - /// See: https://github.com/getsentry/sentry-cli/issues/980 - /// See: https://github.com/getsentry/sentry-cli/issues/1056 - pub fn strip_debug_ids(&mut self) { - for r in self.0.values_mut() { - r.debug_id = None; - } - } -} - impl<'a, T> FromIterator for AssembleDifsRequest<'a> where T: Into>, diff --git a/src/api/data_types/chunking/upload/capability.rs b/src/api/data_types/chunking/upload/capability.rs index 28ab96a249..b79f62c761 100644 --- a/src/api/data_types/chunking/upload/capability.rs +++ b/src/api/data_types/chunking/upload/capability.rs @@ -2,34 +2,6 @@ use serde::{Deserialize, Deserializer}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ChunkUploadCapability { - /// Chunked upload of debug files - DebugFiles, - - /// Chunked upload of release files - ReleaseFiles, - - /// Chunked upload of standalone artifact bundles - ArtifactBundles, - - /// Like `ArtifactBundles`, but with deduplicated chunk - /// upload. - ArtifactBundlesV2, - - /// Upload of PDBs and debug id overrides - Pdbs, - - /// Upload of Portable PDBs - PortablePdbs, - - /// Uploads of source archives - Sources, - - /// Upload of BCSymbolMap and PList auxiliary DIFs - BcSymbolmap, - - /// Upload of il2cpp line mappings - Il2Cpp, - /// Upload of Dart symbol maps DartSymbolMap, @@ -49,15 +21,6 @@ impl<'de> Deserialize<'de> for ChunkUploadCapability { D: Deserializer<'de>, { Ok(match String::deserialize(deserializer)?.as_str() { - "debug_files" => ChunkUploadCapability::DebugFiles, - "release_files" => ChunkUploadCapability::ReleaseFiles, - "artifact_bundles" => ChunkUploadCapability::ArtifactBundles, - "artifact_bundles_v2" => ChunkUploadCapability::ArtifactBundlesV2, - "pdbs" => ChunkUploadCapability::Pdbs, - "portablepdbs" => ChunkUploadCapability::PortablePdbs, - "sources" => ChunkUploadCapability::Sources, - "bcsymbolmaps" => ChunkUploadCapability::BcSymbolmap, - "il2cpp" => ChunkUploadCapability::Il2Cpp, "dartsymbolmap" => ChunkUploadCapability::DartSymbolMap, "preprod_artifacts" => ChunkUploadCapability::PreprodArtifacts, "proguard" => ChunkUploadCapability::Proguard, diff --git a/src/api/data_types/chunking/upload/options.rs b/src/api/data_types/chunking/upload/options.rs index 76a1767bca..1afec7e9a2 100644 --- a/src/api/data_types/chunking/upload/options.rs +++ b/src/api/data_types/chunking/upload/options.rs @@ -23,7 +23,7 @@ pub struct ChunkServerOptions { pub concurrency: u8, #[serde(default)] pub compression: Vec, - #[serde(default = "default_chunk_upload_accept")] + #[serde(default)] pub accept: Vec, } @@ -32,18 +32,4 @@ impl ChunkServerOptions { pub fn supports(&self, capability: ChunkUploadCapability) -> bool { self.accept.contains(&capability) } - - /// Determines whether we need to strip debug_ids from the requests. We need - /// to strip the debug_ids whenever the server does not support chunked - /// uploading of PDBs, to maintain backwards compatibility. - /// - /// See: https://github.com/getsentry/sentry-cli/issues/980 - /// See: https://github.com/getsentry/sentry-cli/issues/1056 - pub fn should_strip_debug_ids(&self) -> bool { - !self.supports(ChunkUploadCapability::DebugFiles) - } -} - -fn default_chunk_upload_accept() -> Vec { - vec![ChunkUploadCapability::DebugFiles] } diff --git a/src/api/errors/api_error.rs b/src/api/errors/api_error.rs index 775f46d509..be6dda24fe 100644 --- a/src/api/errors/api_error.rs +++ b/src/api/errors/api_error.rs @@ -28,8 +28,6 @@ pub(in crate::api) enum ApiErrorKind { ProjectNotFound, #[error("Release not found. Ensure that you configured the correct release, project, and organization.")] ReleaseNotFound, - #[error("chunk upload endpoint not supported by sentry server")] - ChunkUploadNotSupported, #[error("API request failed")] RequestFailed, #[error("could not compress data")] @@ -63,6 +61,9 @@ impl ApiError { } } + // This method is currently only used in the macOS binary, there is no reason + // why not to expose it on other platforms, if we ever need it. + #[cfg(target_os = "macos")] pub(in crate::api) fn kind(&self) -> ApiErrorKind { self.inner } diff --git a/src/api/mod.rs b/src/api/mod.rs index 418d17749c..0a09334c26 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -13,11 +13,9 @@ mod pagination; use std::borrow::Cow; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; -use std::ffi::OsStr; +use std::collections::HashMap; use std::fs::File; use std::io::{self, Read as _, Write}; -use std::path::Path; use std::rc::Rc; use std::sync::Arc; use std::{fmt, thread}; @@ -37,7 +35,6 @@ use log::{debug, info, warn}; use parking_lot::Mutex; use regex::{Captures, Regex}; use secrecy::ExposeSecret as _; -use sentry::protocol::{Exception, Values}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use sha1_smol::Digest; @@ -47,13 +44,11 @@ use uuid::Uuid; use crate::api::errors::{ProjectRenamedError, RetryError}; use crate::config::{Auth, Config}; -use crate::constants::{ARCH, DEFAULT_URL, EXT, PLATFORM, RELEASE_REGISTRY_LATEST_URL, VERSION}; -use crate::utils::file_upload::LegacyUploadContext; +use crate::constants::{ARCH, EXT, PLATFORM, RELEASE_REGISTRY_LATEST_URL, VERSION}; use crate::utils::http::{self, is_absolute_url}; use crate::utils::non_empty::NonEmptySlice; use crate::utils::progress::{ProgressBar, ProgressBarMode}; use crate::utils::retry::{get_default_backoff, DurationAsMilliseconds as _}; -use crate::utils::sourcemaps::get_sourcemap_reference_from_headers; use crate::utils::ui::{capitalize_string, make_byte_progress_bar}; use self::pagination::Pagination; @@ -89,12 +84,6 @@ pub struct AuthenticatedApi<'a> { api: &'a Api, } -pub struct RegionSpecificApi<'a> { - api: &'a AuthenticatedApi<'a>, - org: &'a str, - region_url: Option>, -} - /// Represents an HTTP method that is used by the API. #[derive(Eq, PartialEq, Debug)] pub enum Method { @@ -286,6 +275,10 @@ impl Api { } /// Convenience method that downloads a file into the given file object. + /// + /// Currently only used on macOS, but we could make it available on other platforms + /// if needed. + #[cfg(target_os = "macos")] pub fn download(&self, url: &str, dst: &mut File) -> ApiResult { self.request(Method::Get, url, None)? .follow_location(true)? @@ -445,7 +438,7 @@ impl Api { } } -impl<'a> AuthenticatedApi<'a> { +impl AuthenticatedApi<'_> { // Pass through low-level methods to API. /// Convenience method to call self.api.get. @@ -481,203 +474,6 @@ impl<'a> AuthenticatedApi<'a> { self.get("/")?.convert() } - /// Lists release files for the given `release`, filtered by a set of checksums. - /// When empty checksums list is provided, fetches all possible artifacts. - pub fn list_release_files_by_checksum( - &self, - org: &str, - project: Option<&str>, - release: &str, - checksums: &[String], - ) -> ApiResult> { - let mut rv = vec![]; - let mut cursor = "".to_owned(); - loop { - let mut path = if let Some(project) = project { - format!( - "/projects/{}/{}/releases/{}/files/?cursor={}", - PathArg(org), - PathArg(project), - PathArg(release), - QueryArg(&cursor), - ) - } else { - format!( - "/organizations/{}/releases/{}/files/?cursor={}", - PathArg(org), - PathArg(release), - QueryArg(&cursor), - ) - }; - - let mut checksums_qs = String::new(); - for checksum in checksums.iter() { - checksums_qs.push_str(&format!("&checksum={}", QueryArg(checksum))); - } - // We have a 16kb buffer for reach request configured in nginx, - // so do not even bother trying if it's too long. - // (16_384 limit still leaves us with 384 bytes for the url itself). - if !checksums_qs.is_empty() && checksums_qs.len() <= 16_000 { - path.push_str(&checksums_qs); - } - - let resp = self.get(&path)?; - if resp.status() == 404 || (resp.status() == 400 && !cursor.is_empty()) { - if rv.is_empty() { - return Err(ApiErrorKind::ReleaseNotFound.into()); - } else { - break; - } - } - - let pagination = resp.pagination(); - rv.extend(resp.convert::>()?); - if let Some(next) = pagination.into_next_cursor() { - cursor = next; - } else { - break; - } - } - Ok(rv) - } - - /// Lists all the release files for the given `release`. - pub fn list_release_files( - &self, - org: &str, - project: Option<&str>, - release: &str, - ) -> ApiResult> { - self.list_release_files_by_checksum(org, project, release, &[]) - } - - /// Get a single release file and store it inside provided descriptor. - pub fn get_release_file( - &self, - org: &str, - project: Option<&str>, - version: &str, - file_id: &str, - file_desc: &mut File, - ) -> Result<(), ApiError> { - let path = if let Some(project) = project { - format!( - "/projects/{}/{}/releases/{}/files/{}/?download=1", - PathArg(org), - PathArg(project), - PathArg(version), - PathArg(file_id) - ) - } else { - format!( - "/organizations/{}/releases/{}/files/{}/?download=1", - PathArg(org), - PathArg(version), - PathArg(file_id) - ) - }; - - let resp = self.api.download(&path, file_desc)?; - if resp.status() == 404 { - resp.convert_rnf(ApiErrorKind::ResourceNotFound) - } else { - Ok(()) - } - } - - /// Get a single release file metadata. - pub fn get_release_file_metadata( - &self, - org: &str, - project: Option<&str>, - version: &str, - file_id: &str, - ) -> ApiResult> { - let path = if let Some(project) = project { - format!( - "/projects/{}/{}/releases/{}/files/{}/", - PathArg(org), - PathArg(project), - PathArg(version), - PathArg(file_id) - ) - } else { - format!( - "/organizations/{}/releases/{}/files/{}/", - PathArg(org), - PathArg(version), - PathArg(file_id) - ) - }; - - let resp = self.get(&path)?; - if resp.status() == 404 { - Ok(None) - } else { - resp.convert() - } - } - - /// Deletes a single release file. Returns `true` if the file was - /// deleted or `false` otherwise. - pub fn delete_release_file( - &self, - org: &str, - project: Option<&str>, - version: &str, - file_id: &str, - ) -> ApiResult { - let path = if let Some(project) = project { - format!( - "/projects/{}/{}/releases/{}/files/{}/", - PathArg(org), - PathArg(project), - PathArg(version), - PathArg(file_id) - ) - } else { - format!( - "/organizations/{}/releases/{}/files/{}/", - PathArg(org), - PathArg(version), - PathArg(file_id) - ) - }; - - let resp = self.delete(&path)?; - if resp.status() == 404 { - Ok(false) - } else { - resp.into_result().map(|_| true) - } - } - - /// Deletes all release files. Returns `true` if files were - /// deleted or `false` otherwise. - pub fn delete_release_files( - &self, - org: &str, - project: Option<&str>, - version: &str, - ) -> ApiResult<()> { - let path = if let Some(project) = project { - format!( - "/projects/{}/{}/files/source-maps/?name={}", - PathArg(org), - PathArg(project), - PathArg(version) - ) - } else { - format!( - "/organizations/{}/files/source-maps/?name={}", - PathArg(org), - PathArg(version) - ) - }; - - self.delete(&path)?.into_result().map(|_| ()) - } - /// Creates a new release. pub fn new_release(&self, org: &str, release: &NewRelease) -> ApiResult { // for single project releases use the legacy endpoint that is project bound. @@ -927,50 +723,11 @@ impl<'a> AuthenticatedApi<'a> { .map(|_| true) } - /// Given a list of checksums for DIFs, this returns a list of those - /// that do not exist for the project yet. - pub fn find_missing_dif_checksums( - &self, - org: &str, - project: &str, - checksums: I, - ) -> ApiResult> - where - I: IntoIterator, - { - let mut url = format!( - "/projects/{}/{}/files/dsyms/unknown/?", - PathArg(org), - PathArg(project) - ); - for (idx, checksum) in checksums.into_iter().enumerate() { - if idx > 0 { - url.push('&'); - } - url.push_str("checksums="); - url.push_str(&checksum.to_string()); - } - - let state: MissingChecksumsResponse = self.get(&url)?.convert()?; - Ok(state.missing) - } - /// Get the server configuration for chunked file uploads. - pub fn get_chunk_upload_options(&self, org: &str) -> ApiResult> { + pub fn get_chunk_upload_options(&self, org: &str) -> ApiResult { let url = format!("/organizations/{}/chunk-upload/", PathArg(org)); - match self - .get(&url)? - .convert_rnf::(ApiErrorKind::ChunkUploadNotSupported) - { - Ok(options) => Ok(Some(options)), - Err(error) => { - if error.kind() == ApiErrorKind::ChunkUploadNotSupported { - Ok(None) - } else { - Err(error) - } - } - } + self.get(&url)? + .convert_rnf::(ApiErrorKind::OrganizationNotFound) } /// Request DIF assembling and processing from chunks. @@ -992,32 +749,6 @@ impl<'a> AuthenticatedApi<'a> { .convert_rnf(ApiErrorKind::ProjectNotFound) } - #[deprecated = "release bundle uploads are deprecated in favor of artifact bundle uploads"] - pub fn assemble_release_artifacts( - &self, - org: &str, - release: &str, - checksum: Digest, - chunks: &[Digest], - ) -> ApiResult { - let url = format!( - "/organizations/{}/releases/{}/assemble/", - PathArg(org), - PathArg(release) - ); - - self.request(Method::Post, &url)? - .with_json_body(&ChunkedArtifactRequest { - checksum, - chunks, - projects: &[], - version: None, - dist: None, - })? - .send()? - .convert_rnf(ApiErrorKind::ReleaseNotFound) - } - pub fn assemble_artifact_bundle( &self, org: &str, @@ -1059,36 +790,6 @@ impl<'a> AuthenticatedApi<'a> { .convert_rnf(ApiErrorKind::ProjectNotFound) } - pub fn associate_proguard_mappings( - &self, - org: &str, - project: &str, - data: &AssociateProguard, - ) -> ApiResult<()> { - let path = format!( - "/projects/{}/{}/files/proguard-artifact-releases", - PathArg(org), - PathArg(project) - ); - let resp: ApiResponse = self - .request(Method::Post, &path)? - .with_json_body(data)? - .send()?; - if resp.status() == 201 { - Ok(()) - } else if resp.status() == 409 { - info!( - "Release association for release '{}', UUID '{}' already exists.", - data.release_name, data.proguard_uuid - ); - Ok(()) - } else if resp.status() == 404 { - Err(ApiErrorKind::ResourceNotFound.into()) - } else { - resp.convert() - } - } - /// List all organizations associated with the authenticated token /// in the given `Region`. If no `Region` is provided, we assume /// we're issuing a request to a monolith deployment. @@ -1342,91 +1043,6 @@ impl<'a> AuthenticatedApi<'a> { } Ok(rv) } - - /// Looks up an event, which was already processed by Sentry and returns it. - /// If it does not exist `None` will be returned. - pub fn get_event( - &self, - org: &str, - project: Option<&str>, - event_id: &str, - ) -> ApiResult> { - let path = if let Some(project) = project { - format!( - "/projects/{}/{}/events/{}/json/", - PathArg(org), - PathArg(project), - PathArg(event_id) - ) - } else { - format!( - "/organizations/{}/events/{}/json/", - PathArg(org), - PathArg(event_id) - ) - }; - - let resp = self.get(&path)?; - if resp.status() == 404 { - Ok(None) - } else { - resp.convert() - } - } - - fn get_region_url(&self, org: &str) -> ApiResult { - self.get(&format!("/organizations/{org}/region/")) - .and_then(|resp| resp.convert::()) - .map(|region| region.url) - } - - pub fn region_specific(&'a self, org: &'a str) -> RegionSpecificApi<'a> { - let base_url = self.api.config.get_base_url(); - if base_url.is_err() - || base_url.expect("base_url should not be error") != DEFAULT_URL.trim_end_matches('/') - { - // Do not specify a region URL unless the URL is configured to https://sentry.io (i.e. the default). - return RegionSpecificApi { - api: self, - org, - region_url: None, - }; - } - - let region_url = match self - .api - .config - .get_auth() - .expect("auth should not be None for authenticated API!") - { - Auth::Token(token) => match token.payload() { - Some(payload) => Some(payload.region_url.clone().into()), - None => { - let region_url = self.get_region_url(org); - if let Err(err) = ®ion_url { - log::warn!("Failed to get region URL due to following error: {err}"); - log::info!("Failling back to the default region."); - } - - region_url.ok().map(|url| url.into()) - } - }, - #[expect(deprecated, reason = "Auth key is deprecated.")] - Auth::Key(_) => { - log::warn!( - "Auth key is not supported for region-specific API. Falling back to default region." - ); - - None - } - }; - - RegionSpecificApi { - api: self, - org, - region_url, - } - } } /// Available datasets for fetching organization events @@ -1500,89 +1116,6 @@ impl FetchEventsOptions<'_> { } } -impl RegionSpecificApi<'_> { - fn request(&self, method: Method, url: &str) -> ApiResult { - self.api - .api - .request(method, url, self.region_url.as_deref()) - } - - /// Uploads a ZIP archive containing DIFs from the given path. - pub fn upload_dif_archive(&self, project: &str, file: &Path) -> ApiResult> { - let path = format!( - "/projects/{}/{}/files/dsyms/", - PathArg(self.org), - PathArg(project) - ); - let mut form = curl::easy::Form::new(); - form.part("file").file(file).add()?; - self.request(Method::Post, &path)? - .with_form_data(form)? - .progress_bar_mode(ProgressBarMode::Request) - .send()? - .convert() - } - - /// Uploads a new release file. The file is loaded directly from the file - /// system and uploaded as `name`. - pub fn upload_release_file( - &self, - context: &LegacyUploadContext, - contents: &[u8], - name: &str, - headers: Option<&[(String, String)]>, - progress_bar_mode: ProgressBarMode, - ) -> ApiResult> { - let path = if let Some(project) = context.project() { - format!( - "/projects/{}/{}/releases/{}/files/", - PathArg(context.org()), - PathArg(project), - PathArg(context.release()) - ) - } else { - format!( - "/organizations/{}/releases/{}/files/", - PathArg(context.org()), - PathArg(context.release()) - ) - }; - - let mut form = curl::easy::Form::new(); - - let filename = Path::new(name) - .file_name() - .and_then(OsStr::to_str) - .unwrap_or("unknown.bin"); - form.part("file") - .buffer(filename, contents.to_vec()) - .add()?; - form.part("name").contents(name.as_bytes()).add()?; - if let Some(dist) = context.dist() { - form.part("dist").contents(dist.as_bytes()).add()?; - } - - if let Some(headers) = headers { - for (key, value) in headers { - form.part("header") - .contents(format!("{key}:{value}").as_bytes()) - .add()?; - } - } - - let resp = self - .request(Method::Post, &path)? - .with_form_data(form)? - .progress_bar_mode(progress_bar_mode) - .send()?; - if resp.status() == 409 { - Ok(None) - } else { - resp.convert_rnf(ApiErrorKind::ReleaseNotFound) - } - } -} - fn send_req( handle: &mut curl::easy::Easy, out: &mut W, @@ -1631,19 +1164,9 @@ fn handle_req( } else if progress_bar_mode.active() { let pb_progress = pb.clone(); #[expect(clippy::unwrap_used, reason = "legacy code")] - handle.progress_function(move |a, b, c, d| { - let (down_len, down_pos, up_len, up_pos) = (a as u64, b as u64, c as u64, d as u64); + handle.progress_function(move |a, b, _, _| { + let (down_len, down_pos) = (a as u64, b as u64); let mut pb = pb_progress.borrow_mut(); - if up_len > 0 && progress_bar_mode.request() { - if up_pos < up_len { - if pb.is_none() { - *pb = Some(make_byte_progress_bar(up_len)); - } - pb.as_ref().unwrap().set_position(up_pos); - } else if pb.is_some() { - pb.take().unwrap().finish_and_clear(); - } - } if down_len > 0 && progress_bar_mode.response() { if down_pos < down_len { if pb.is_none() { @@ -1772,12 +1295,6 @@ impl ApiRequest { pub fn with_auth(mut self, auth: &Auth) -> ApiResult { self.is_authenticated = true; match *auth { - #[expect(deprecated, reason = "API key is deprecated.")] - Auth::Key(ref key) => { - self.handle.username(key)?; - debug!("using deprecated key based authentication"); - Ok(self) - } Auth::Token(ref token) => { debug!("using token authentication"); self.with_header( @@ -2092,23 +1609,6 @@ pub struct AuthInfo { pub user: Option, } -/// A release artifact -#[derive(Clone, Deserialize, Debug)] -pub struct Artifact { - pub id: String, - pub sha1: String, - pub name: String, - pub size: u64, - pub dist: Option, - pub headers: HashMap, -} - -impl Artifact { - pub fn get_sourcemap_reference(&self) -> Option<&str> { - get_sourcemap_reference_from_headers(self.headers.iter()) - } -} - /// Information for new releases #[derive(Debug, Serialize, Default)] pub struct NewRelease { @@ -2246,17 +1746,6 @@ impl DebugInfoFile { } } -#[derive(Debug, Serialize)] -pub struct AssociateProguard { - pub release_name: String, - pub proguard_uuid: String, -} - -#[derive(Deserialize)] -struct MissingChecksumsResponse { - missing: HashSet, -} - #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Issue { @@ -2444,12 +1933,6 @@ pub struct ProcessedEvent { #[expect(dead_code)] pub project: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub release: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub dist: Option, - #[serde(default, skip_serializing_if = "Values::is_empty")] - pub exception: Values, - #[serde(default, skip_serializing_if = "Option::is_none")] pub user: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tags: Option>, diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index ac2c770cb9..d272ede3ba 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -557,12 +557,7 @@ fn upload_file( build_configuration.unwrap_or("unknown"), ); - let chunk_upload_options = api.get_chunk_upload_options(org)?.ok_or_else(|| { - anyhow!( - "The Sentry server lacks chunked uploading support, which \ - is required for build uploads. {SELF_HOSTED_ERROR_HINT}" - ) - })?; + let chunk_upload_options = api.get_chunk_upload_options(org)?; if !chunk_upload_options.supports(ChunkUploadCapability::PreprodArtifacts) { bail!( diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 297a8db65e..2784060806 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -132,10 +132,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { ))?; let chunk_upload_options = api .authenticated()? - .get_chunk_upload_options(org)? - .ok_or_else(|| anyhow::anyhow!( - "server does not support chunked uploading. Please update your Sentry server." - ))?; + .get_chunk_upload_options(org)?; if !chunk_upload_options.supports(ChunkUploadCapability::DartSymbolMap) { bail!( diff --git a/src/commands/files/delete.rs b/src/commands/files/delete.rs deleted file mode 100644 index 39a393fe9f..0000000000 --- a/src/commands/files/delete.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::collections::HashSet; - -use anyhow::Result; -use clap::{Arg, ArgAction, ArgMatches, Command}; - -use crate::api::Api; -use crate::config::Config; - -pub fn make_command(command: Command) -> Command { - command - .about("[DEPRECATED] Delete a release file.") - .hide(true) - // Backward compatibility with `releases files ` commands. - .arg(Arg::new("version").long("version").hide(true)) - .arg( - Arg::new("names") - .value_name("NAMES") - .num_args(1..) - .action(ArgAction::Append) - .help("Filenames to delete."), - ) - .arg( - Arg::new("all") - .short('A') - .long("all") - .action(ArgAction::SetTrue) - .help("Delete all files."), - ) -} - -pub fn execute(matches: &ArgMatches) -> Result<()> { - let config = Config::current(); - let release = config.get_release_with_legacy_fallback(matches)?; - let org = config.get_org(matches)?; - let project = config.get_project(matches).ok(); - let api = Api::current(); - let authenticated_api = api.authenticated()?; - - if matches.get_flag("all") { - authenticated_api.delete_release_files(&org, project.as_deref(), &release)?; - println!("All files deleted."); - return Ok(()); - } - - let files: HashSet = match matches.get_many::("names") { - Some(paths) => paths.map(|x| x.into()).collect(), - None => HashSet::new(), - }; - for file in authenticated_api.list_release_files(&org, project.as_deref(), &release)? { - if !files.contains(&file.name) { - continue; - } - if authenticated_api.delete_release_file(&org, project.as_deref(), &release, &file.id)? { - println!("D {}", file.name); - } - } - Ok(()) -} diff --git a/src/commands/files/list.rs b/src/commands/files/list.rs deleted file mode 100644 index 72e98ab1b5..0000000000 --- a/src/commands/files/list.rs +++ /dev/null @@ -1,52 +0,0 @@ -use anyhow::Result; -use clap::{Arg, ArgMatches, Command}; -use indicatif::HumanBytes; - -use crate::{api::Api, config::Config, utils::formatting::Table}; - -pub fn make_command(command: Command) -> Command { - command - .about("[DEPRECATED] List all release files.") - .hide(true) - // Backward compatibility with `releases files ` commands. - .arg(Arg::new("version").long("version").hide(true)) -} - -pub fn execute(matches: &ArgMatches) -> Result<()> { - let config = Config::current(); - let release = config.get_release_with_legacy_fallback(matches)?; - let org = config.get_org(matches)?; - let project = config.get_project(matches).ok(); - let api = Api::current(); - - let mut table = Table::new(); - table - .title_row() - .add("Name") - .add("Distribution") - .add("Source Map") - .add("Size"); - - for artifact in api - .authenticated()? - .list_release_files(&org, project.as_deref(), &release)? - { - let row = table.add_row(); - row.add(&artifact.name); - if let Some(ref dist) = artifact.dist { - row.add(dist); - } else { - row.add(""); - } - if let Some(sm_ref) = artifact.get_sourcemap_reference() { - row.add(sm_ref); - } else { - row.add(""); - } - row.add(HumanBytes(artifact.size)); - } - - table.print(); - - Ok(()) -} diff --git a/src/commands/files/mod.rs b/src/commands/files/mod.rs deleted file mode 100644 index 74b7edbc36..0000000000 --- a/src/commands/files/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -use anyhow::Result; -use clap::{ArgMatches, Command}; -use console::style; - -use crate::utils::args::ArgExt as _; - -pub mod delete; -pub mod list; -pub mod upload; - -macro_rules! each_subcommand { - ($mac:ident) => { - $mac!(delete); - $mac!(list); - $mac!(upload); - }; -} - -pub fn make_command(mut command: Command) -> Command { - macro_rules! add_subcommand { - ($name:ident) => {{ - command = command.subcommand(crate::commands::files::$name::make_command( - Command::new(stringify!($name).replace('_', "-")), - )); - }}; - } - - command = command - .about("[DEPRECATED] Manage release artifacts.") - .hide(true) - .subcommand_required(true) - .arg_required_else_help(true) - .org_arg() - .project_arg(true) - .release_arg() - // Backward compatibility with `releases files upload-sourcemaps` commands. - .subcommand( - crate::commands::sourcemaps::upload::make_command(Command::new("upload-sourcemaps")) - .hide(true), - ); - - each_subcommand!(add_subcommand); - command -} - -pub fn execute(matches: &ArgMatches) -> Result<()> { - eprintln!("{}", style("⚠ DEPRECATION NOTICE: This functionality will be removed in a future version of `sentry-cli`. \ - Use the `sourcemaps` command instead.").yellow()); - - macro_rules! execute_subcommand { - ($name:ident) => {{ - if let Some(sub_matches) = - matches.subcommand_matches(&stringify!($name).replace('_', "-")) - { - return crate::commands::files::$name::execute(&sub_matches); - } - }}; - } - each_subcommand!(execute_subcommand); - - // To preserve backward compatibility - if let Some(sub_matches) = matches.subcommand_matches("upload-sourcemaps") { - return crate::commands::sourcemaps::upload::execute(sub_matches); - } - - unreachable!(); -} diff --git a/src/commands/files/upload.rs b/src/commands/files/upload.rs deleted file mode 100644 index 469211e4f4..0000000000 --- a/src/commands/files/upload.rs +++ /dev/null @@ -1,268 +0,0 @@ -#![expect(clippy::unwrap_used, reason = "contains legacy code which uses unwrap")] - -use std::collections::BTreeMap; -use std::ffi::OsStr; -use std::fs; -use std::io::Read as _; -use std::path::Path; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::{bail, format_err, Result}; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use log::warn; -use symbolic::debuginfo::sourcebundle::SourceFileType; - -use crate::api::Api; -use crate::config::Config; -use crate::constants::DEFAULT_MAX_WAIT; -use crate::utils::args::validate_distribution; -use crate::utils::file_search::ReleaseFileSearch; -use crate::utils::file_upload::{ - initialize_legacy_release_upload, FileUpload, SourceFile, UploadContext, -}; -use crate::utils::fs::{decompress_gzip_content, is_gzip_compressed, path_as_url}; -use crate::utils::progress::ProgressBarMode; - -pub fn make_command(command: Command) -> Command { - command - .about("[DEPRECATED] Upload files for a release.") - .hide(true) - // Backward compatibility with `releases files ` commands. - .arg(Arg::new("version").long("version").hide(true)) - .arg( - Arg::new("path") - .value_name("PATH") - .required(true) - .help("The path to the file or directory to upload."), - ) - .arg( - Arg::new("name") - .value_name("NAME") - .help("The name of the file on the server."), - ) - .arg( - Arg::new("dist") - .long("dist") - .short('d') - .value_name("DISTRIBUTION") - .value_parser(validate_distribution) - .help("Optional distribution identifier for this file."), - ) - .arg( - Arg::new("decompress") - .long("decompress") - .action(ArgAction::SetTrue) - .help("Enable files gzip decompression prior to upload."), - ) - .arg( - Arg::new("wait") - .long("wait") - .action(ArgAction::SetTrue) - .conflicts_with("wait_for") - .help("Wait for the server to fully process uploaded files."), - ) - .arg( - Arg::new("wait_for") - .long("wait-for") - .value_name("SECS") - .value_parser(clap::value_parser!(u64)) - .conflicts_with("wait") - .help( - "Wait for the server to fully process uploaded files, \ - but at most for the given number of seconds.", - ), - ) - .arg( - Arg::new("file-headers") - .long("file-header") - .short('H') - .value_name("KEY VALUE") - .action(ArgAction::Append) - .help("Store a header with this file."), - ) - .arg( - Arg::new("url_prefix") - .short('u') - .long("url-prefix") - .value_name("PREFIX") - .help("The URL prefix to prepend to all filenames."), - ) - .arg( - Arg::new("url_suffix") - .long("url-suffix") - .value_name("SUFFIX") - .help("The URL suffix to append to all filenames."), - ) - .arg( - Arg::new("ignore") - .long("ignore") - .short('i') - .value_name("IGNORE") - .action(ArgAction::Append) - .help("Ignores all files and folders matching the given glob"), - ) - .arg( - Arg::new("ignore_file") - .long("ignore-file") - .short('I') - .value_name("IGNORE_FILE") - .help( - "Ignore all files and folders specified in the given \ - ignore file, e.g. .gitignore.", - ), - ) - .arg( - Arg::new("extensions") - .long("ext") - .short('x') - .value_name("EXT") - .action(ArgAction::Append) - .help( - "Set the file extensions that are considered for upload. \ - This overrides the default extensions. To add an extension, all default \ - extensions must be repeated. Specify once per extension.", - ), - ) -} - -pub fn execute(matches: &ArgMatches) -> Result<()> { - let config = Config::current(); - let release = config.get_release_with_legacy_fallback(matches)?; - let org = config.get_org(matches)?; - let project = config.get_project(matches).ok(); - let api = Api::current(); - let authenticated_api = api.authenticated()?; - let chunk_upload_options = authenticated_api.get_chunk_upload_options(&org)?; - - let dist = matches.get_one::("dist").map(String::as_str); - let mut headers = BTreeMap::new(); - if let Some(header_list) = matches.get_many::("file-headers") { - for header in header_list { - if !header.contains(':') { - bail!("Invalid header. Needs to be in key:value format"); - } - let (key, value) = header.split_once(':').unwrap(); - headers.insert(key.trim().to_owned(), value.trim().to_owned()); - } - }; - - let wait_for_secs = matches.get_one::("wait_for").copied(); - let wait = matches.get_flag("wait") || wait_for_secs.is_some(); - let max_wait = wait_for_secs.map_or(DEFAULT_MAX_WAIT, Duration::from_secs); - - let context = &UploadContext { - org: &org, - projects: project.as_slice().try_into().ok(), - release: Some(&release), - dist, - note: None, - wait, - max_wait, - chunk_upload_options: chunk_upload_options.as_ref(), - }; - - let path = Path::new(matches.get_one::("path").unwrap()); - // Batch files upload - if path.is_dir() { - let ignore_file = matches - .get_one::("ignore_file") - .map(String::as_str) - .unwrap_or_default(); - let ignores: Vec<_> = matches - .get_many::("ignore") - .map(|ignores| ignores.map(|i| format!("!{i}")).collect()) - .unwrap_or_default(); - let extensions: Vec<_> = matches - .get_many::("extensions") - .map(|extensions| extensions.map(|ext| ext.trim_start_matches('.')).collect()) - .unwrap_or_default(); - - let sources = ReleaseFileSearch::new(path.to_path_buf()) - .ignore_file(ignore_file) - .ignores(ignores) - .extensions(extensions) - .decompress(matches.get_flag("decompress")) - .collect_files()?; - - let url_suffix = matches - .get_one::("url_suffix") - .map(String::as_str) - .unwrap_or_default(); - let mut url_prefix = matches - .get_one::("url_prefix") - .map(String::as_str) - .unwrap_or("~"); - // remove a single slash from the end. so ~/ becomes ~ and app:/// becomes app:// - if url_prefix.ends_with('/') { - url_prefix = &url_prefix[..url_prefix.len() - 1]; - } - let files = sources - .iter() - .map(|source| { - let local_path = source.path.strip_prefix(&source.base_path).unwrap(); - let url = format!("{url_prefix}/{}{url_suffix}", path_as_url(local_path)); - - ( - url.clone(), - SourceFile { - url, - path: source.path.clone(), - contents: Arc::new(source.contents.clone()), - ty: SourceFileType::Source, - headers: headers.clone(), - messages: vec![], - already_uploaded: false, - }, - ) - }) - .collect(); - - FileUpload::new(context).files(&files).upload() - } - // Single file upload - else { - initialize_legacy_release_upload(context)?; - - let name = match matches.get_one::("name") { - Some(name) => name, - None => Path::new(path) - .file_name() - .and_then(OsStr::to_str) - .ok_or_else(|| format_err!("No filename provided."))?, - }; - - let mut f = fs::File::open(path)?; - let mut contents = Vec::new(); - f.read_to_end(&mut contents)?; - - if matches.get_flag("decompress") && is_gzip_compressed(&contents) { - contents = decompress_gzip_content(&contents).unwrap_or_else(|_| { - warn!("Could not decompress: {name}"); - contents - }); - } - - if let Some(artifact) = authenticated_api - .region_specific(context.org) - .upload_release_file( - &context.try_into()?, - &contents, - name, - Some( - headers - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>() - .as_slice(), - ), - ProgressBarMode::Request, - )? - { - println!("A {} ({} bytes)", artifact.sha1, artifact.size); - } else { - bail!("File already present!"); - } - Ok(()) - } -} diff --git a/src/commands/info.rs b/src/commands/info.rs index 8ba9f3ef56..7d37f06f67 100644 --- a/src/commands/info.rs +++ b/src/commands/info.rs @@ -59,8 +59,6 @@ fn describe_auth(auth: Option<&Auth>) -> &str { match auth { None => "Unauthorized", Some(&Auth::Token(_)) => "Auth Token", - #[expect(deprecated, reason = "API key is deprecated.")] - Some(&Auth::Key(_)) => "API Key (deprecated)", } } @@ -75,8 +73,6 @@ fn get_config_status_json() -> Result<()> { rv.auth.auth_type = config.get_auth().map(|val| match val { Auth::Token(_) => "token".into(), - #[expect(deprecated, reason = "API key is deprecated.")] - Auth::Key(_) => "api_key".into(), }); rv.auth.successful = config.get_auth().is_some() && Api::current().authenticated()?.get_auth_info().is_ok(); diff --git a/src/commands/login.rs b/src/commands/login.rs index 00754e56d1..eed0e7738f 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -140,8 +140,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { fn get_org_from_auth(auth: &Auth) -> Option<&str> { match auth { Auth::Token(token) => get_org_from_token(token), - #[expect(deprecated, reason = "API key is deprecated.")] - Auth::Key(_) => None, } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 522c1581b6..330ea7383f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -26,7 +26,6 @@ mod debug_files; mod deploys; mod derive_parser; mod events; -mod files; mod info; mod issues; mod login; @@ -56,7 +55,6 @@ macro_rules! each_subcommand { $mac!(debug_files); $mac!(deploys); $mac!(events); - $mac!(files); $mac!(info); $mac!(issues); $mac!(login); @@ -137,18 +135,6 @@ fn preexecute_hooks() -> Result { } fn configure_args(config: &mut Config, matches: &ArgMatches) { - if let Some(api_key) = matches.get_one::("api_key") { - log::warn!( - "[DEPRECTATION NOTICE] API key authentication and the --api-key argument are \ - deprecated. \ - Please generate an auth token, and use the --auth-token argument instead." - ); - - #[expect(deprecated, reason = "Auth key is deprecated.")] - let auth = Auth::Key(api_key.to_owned()); - config.set_auth(auth); - } - if let Some(auth_token) = matches.get_one::("auth_token") { config.set_auth(Auth::Token(auth_token.to_owned())); } @@ -192,13 +178,6 @@ fn app() -> Command { .value_parser(auth_token_parser) .help("Use the given Sentry auth token."), ) - .arg( - Arg::new("api_key") - .value_name("API_KEY") - .long("api-key") - .hide(true) - .help("[DEPRECATED] Use the given Sentry API key."), - ) .arg( Arg::new("log_level") .value_name("LOG_LEVEL") diff --git a/src/commands/react_native/appcenter.rs b/src/commands/react_native/appcenter.rs index 8205fec043..b5e271f008 100644 --- a/src/commands/react_native/appcenter.rs +++ b/src/commands/react_native/appcenter.rs @@ -196,13 +196,13 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { processor.upload(&UploadContext { org: &org, - projects: Some(projects.as_non_empty_slice()), + projects: projects.as_non_empty_slice(), release: Some(&release), dist: None, note: None, wait, max_wait, - chunk_upload_options: chunk_upload_options.as_ref(), + chunk_upload_options: &chunk_upload_options, })?; } Some(dists) => { @@ -214,13 +214,13 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { processor.upload(&UploadContext { org: &org, - projects: Some(projects.as_non_empty_slice()), + projects: projects.as_non_empty_slice(), release: Some(&release), dist: Some(dist), note: None, wait, max_wait, - chunk_upload_options: chunk_upload_options.as_ref(), + chunk_upload_options: &chunk_upload_options, })?; } } diff --git a/src/commands/react_native/gradle.rs b/src/commands/react_native/gradle.rs index f8573c6ab4..ff706d601f 100644 --- a/src/commands/react_native/gradle.rs +++ b/src/commands/react_native/gradle.rs @@ -123,26 +123,26 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { processor.upload(&UploadContext { org: &org, - projects: Some(projects.as_non_empty_slice()), + projects: projects.as_non_empty_slice(), release: Some(version), dist: Some(dist), note: None, wait, max_wait, - chunk_upload_options: chunk_upload_options.as_ref(), + chunk_upload_options: &chunk_upload_options, })?; } } else { // Debug Id Upload processor.upload(&UploadContext { org: &org, - projects: Some(projects.as_non_empty_slice()), + projects: projects.as_non_empty_slice(), release: None, dist: None, note: None, wait, max_wait, - chunk_upload_options: chunk_upload_options.as_ref(), + chunk_upload_options: &chunk_upload_options, })?; } diff --git a/src/commands/react_native/xcode.rs b/src/commands/react_native/xcode.rs index a88f23486a..4ff365f34e 100644 --- a/src/commands/react_native/xcode.rs +++ b/src/commands/react_native/xcode.rs @@ -345,13 +345,13 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { if dist_from_env.is_err() && release_from_env.is_err() && matches.get_flag("no_auto_release") { processor.upload(&UploadContext { org: &org, - projects: Some(projects.as_non_empty_slice()), + projects: projects.as_non_empty_slice(), release: None, dist: None, note: None, wait, max_wait, - chunk_upload_options: chunk_upload_options.as_ref(), + chunk_upload_options: &chunk_upload_options, })?; } else { let (dist, release_name) = match (&dist_from_env, &release_from_env) { @@ -380,26 +380,26 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { None => { processor.upload(&UploadContext { org: &org, - projects: Some(projects.as_non_empty_slice()), + projects: projects.as_non_empty_slice(), release: release_name.as_deref(), dist: dist.as_deref(), note: None, wait, max_wait, - chunk_upload_options: chunk_upload_options.as_ref(), + chunk_upload_options: &chunk_upload_options, })?; } Some(dists) => { for dist in dists { processor.upload(&UploadContext { org: &org, - projects: Some(projects.as_non_empty_slice()), + projects: projects.as_non_empty_slice(), release: release_name.as_deref(), dist: Some(dist), note: None, wait, max_wait, - chunk_upload_options: chunk_upload_options.as_ref(), + chunk_upload_options: &chunk_upload_options, })?; } } diff --git a/src/commands/releases/mod.rs b/src/commands/releases/mod.rs index 43a5fd844b..b4dd75fc34 100644 --- a/src/commands/releases/mod.rs +++ b/src/commands/releases/mod.rs @@ -42,13 +42,6 @@ pub fn make_command(mut command: Command) -> Command { .arg_required_else_help(true) .org_arg() .project_arg(true) - // Backward compatibility with `releases files ` commands. - .subcommand( - crate::commands::files::make_command(Command::new("files")) - .allow_hyphen_values(true) - .version_arg(true) - .hide(true), - ) // Backward compatibility with `releases deploys ` commands. .subcommand( crate::commands::deploys::make_command(Command::new("deploys")) @@ -73,10 +66,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { } each_subcommand!(execute_subcommand); - // To preserve backward compatibility - if let Some(sub_matches) = matches.subcommand_matches("files") { - return crate::commands::files::execute(sub_matches); - } // To preserve backward compatibility if let Some(sub_matches) = matches.subcommand_matches("deploys") { return crate::commands::deploys::execute(sub_matches); diff --git a/src/commands/sourcemaps/explain.rs b/src/commands/sourcemaps/explain.rs deleted file mode 100644 index 2e04a0ee3b..0000000000 --- a/src/commands/sourcemaps/explain.rs +++ /dev/null @@ -1,521 +0,0 @@ -#![expect(clippy::unwrap_used, reason = "deprecated command")] - -use std::io::Read as _; -use std::path::Path; - -use anyhow::{bail, format_err, Result}; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use console::style; -use sentry::protocol::{Frame, Stacktrace}; -use url::Url; - -use crate::api::{Api, Artifact, ProcessedEvent}; -use crate::config::Config; -use crate::utils::fs::TempFile; -use crate::utils::system::QuietExit; - -use super::resolve::print_source; - -pub fn make_command(command: Command) -> Command { - command - .about("[DEPRECATED] Explain why sourcemaps are not working for a given event.") - .hide(true) - .alias("why") - .arg( - Arg::new("event") - .value_name("EVENT_ID") - .required(true) - .help("ID of an event to be explained."), - ) - .arg( - Arg::new("frame") - .long("frame") - .default_value("0") - .value_parser(clap::value_parser!(usize)) - .help("Position of the frame that should be used for source map resolution."), - ) - .arg( - Arg::new("force") - .long("force") - .short('f') - .action(ArgAction::SetTrue) - .help("Force full validation flow, even when event is already source mapped."), - ) -} - -fn tip(msg: S) -where - S: std::fmt::Display, -{ - println!("{}", style(format!("ℹ {msg}")).blue()); -} - -fn success(msg: S) -where - S: std::fmt::Display, -{ - println!("{}", style(format!("✔ {msg}")).green()); -} - -fn warning(msg: S) -where - S: std::fmt::Display, -{ - println!("{}", style(format!("⚠ {msg}")).yellow()); -} - -fn error(msg: S) -where - S: std::fmt::Display, -{ - println!("{}", style(format!("✖ {msg}")).red()); -} - -fn fetch_event(org: &str, project: &str, event_id: &str) -> Result { - match Api::current() - .authenticated()? - .get_event(org, Some(project), event_id)? - { - Some(event) => { - success(format!("Fetched data for event: {event_id}")); - Ok(event) - } - None => { - error(format!("Could not retrieve event {event_id}")); - tip("Make sure that event ID you used is valid."); - Err(QuietExit(1).into()) - } - } -} - -fn extract_in_app_frames(stacktrace: &Stacktrace) -> Vec<&Frame> { - stacktrace - .frames - .iter() - .filter(|frame| frame.in_app.unwrap_or(false)) - .collect() -} - -fn extract_nth_frame(stacktrace: &Stacktrace, position: usize) -> Result<&Frame> { - let mut in_app_frames = extract_in_app_frames(stacktrace); - - if in_app_frames.is_empty() { - bail!("Event exception stacktrace has no in_app frames"); - } - - // Frames are in bottom-up order. - in_app_frames.reverse(); - - let frame = in_app_frames - .get(position) - .ok_or_else(|| format_err!("Selected frame ({position}) is missing."))?; - - let abs_path = frame - .abs_path - .as_ref() - .ok_or_else(|| format_err!("Selected frame ({position}) is missing an abs_path"))?; - - if let Ok(abs_path) = Url::parse(abs_path) { - if Path::new(abs_path.path()).extension().is_none() { - bail!("Selected frame ({position}) of event exception originates from the