diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 63009c165f..cc2b33b35b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,15 @@ * @szokeasaurusrex @getsentry/owners-sentry-cli +# Files co-owned by JavaScript SDK team +/.cursor/rules/javascript-development.mdc @getsentry/team-javascript-sdks @getsentry/owners-sentry-cli +/.npmignore @getsentry/team-javascript-sdks @getsentry/owners-sentry-cli +/bin/ @getsentry/team-javascript-sdks @getsentry/owners-sentry-cli # these are the JS "binaries" +/js @getsentry/team-javascript-sdks @getsentry/owners-sentry-cli +/package.json @getsentry/team-javascript-sdks @getsentry/owners-sentry-cli +/scripts/*.js @getsentry/team-javascript-sdks @getsentry/owners-sentry-cli +/tsconfig.json @getsentry/team-javascript-sdks @getsentry/owners-sentry-cli + +# Files co-owned by Emerge Tools team /apple-catalog-parsing @getsentry/emerge-tools @getsentry/owners-sentry-cli /src/commands/build @getsentry/emerge-tools @getsentry/owners-sentry-cli /src/utils/build @getsentry/emerge-tools @getsentry/owners-sentry-cli diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 814f9ce509..549cb102af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: - name: Rename Binary run: mv target/${{ matrix.target }}/release/sentry-cli sentry-cli-Linux-${{ matrix.arch }} - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: artifact-bin-linux-${{ matrix.arch }} path: sentry-cli-Linux-${{ matrix.arch }} @@ -81,7 +81,7 @@ jobs: - name: Rename Binary run: mv target/${{ matrix.target }}/release/sentry-cli sentry-cli-Darwin-${{ matrix.arch }} - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: unsigned-bin-macos-${{ matrix.arch }} path: sentry-cli-Darwin-${{ matrix.arch }} @@ -93,7 +93,7 @@ jobs: runs-on: macos-14 steps: - - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # 6.0.0 with: pattern: unsigned-bin-macos-* merge-multiple: true @@ -101,7 +101,7 @@ jobs: - name: Link universal binary run: lipo -create -output sentry-cli-Darwin-universal sentry-cli-Darwin-x86_64 sentry-cli-Darwin-arm64 - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: unsigned-bin-macos-universal path: sentry-cli-Darwin-universal @@ -146,7 +146,7 @@ jobs: echo "$APPLE_API_KEY" | base64 --decode > ${{ env.APPLE_API_KEY_PATH }} - name: Download unsigned binary - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # 6.0.0 with: name: unsigned-bin-macos-${{ matrix.arch }} @@ -171,7 +171,7 @@ jobs: sentry-cli-Darwin-${{ matrix.arch }}.zip - name: Upload signed binary - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: artifact-bin-macos-${{ matrix.arch }} path: sentry-cli-Darwin-${{ matrix.arch }} @@ -210,7 +210,7 @@ jobs: - name: Rename Binary run: mv target/${{ env.TARGET }}/release/sentry-cli.exe sentry-cli-Windows-${{ matrix.arch }}.exe - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: artifact-bin-windows-${{ matrix.arch }} path: sentry-cli-Windows-${{ matrix.arch }}.exe @@ -229,7 +229,7 @@ jobs: node-version: '20.10.0' - name: Download compiled binaries - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # 6.0.0 with: pattern: artifact-bin-* merge-multiple: true @@ -242,7 +242,7 @@ jobs: - run: npm pack - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: artifact-pkg-node path: '*.tgz' @@ -259,7 +259,7 @@ jobs: with: python-version: '3.11' - run: python3 -m pip install build && python3 -m build - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: python-base path: dist/* @@ -274,18 +274,18 @@ jobs: - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # 6.0.0 with: python-version: '3.11' - - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # 6.0.0 with: pattern: artifact-bin-* merge-multiple: true path: binaries - - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # 6.0.0 with: name: python-base merge-multiple: true path: python-base - run: scripts/wheels --binaries binaries --base python-base --dest dist - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: artifact-pkg-python path: dist/* @@ -300,7 +300,7 @@ jobs: - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # 6.0.0 with: node-version: '20.10.0' - - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # 6.0.0 with: pattern: artifact-bin-* merge-multiple: true @@ -328,7 +328,7 @@ jobs: done - name: Upload packaged npm binary distributions - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: artifact-npm-binary-distributions path: npm-binary-distributions/*/*.tgz @@ -395,7 +395,7 @@ jobs: runs-on: ubuntu-24.04 needs: [linux, sign-macos-binaries, windows, npm-distributions, node, python] steps: - - uses: actions/upload-artifact/merge@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + - uses: actions/upload-artifact/merge@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: # Craft expects release assets to be a single artifact named after the sha. name: ${{ github.sha }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 28126b6dc0..ec860e5009 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -37,7 +37,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # 4.30.9 + uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # 4.31.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -48,7 +48,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # 4.30.9 + uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # 4.31.2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions @@ -59,4 +59,4 @@ jobs: # make bootstrap # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # 4.30.9 + uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # 4.31.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 26df29c70b..b3103b631c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,40 @@ # Changelog -"You know what they say. Fool me once, strike one, but fool me twice... strike three." — Michael Scott +## 2.58.1 + +### Deprecations + +- Deprecated API key authentication ([#2934](https://github.com/getsentry/sentry-cli/pull/2934), [#2937](https://github.com/getsentry/sentry-cli/pull/2937)). Users who are still using API keys to authenticate Sentry CLI should generate and use an [Auth Token](https://docs.sentry.io/account/auth-tokens/) instead. + +### Improvements + +- The `sentry-cli debug-files bundle-jvm` no longer makes any HTTP requests to Sentry, meaning auth tokens are no longer needed, and the command can be run offline ([#2926](https://github.com/getsentry/sentry-cli/pull/2926)). + +### Fixes + +- Skip setting `base_sha` and `base_ref` when they equal `head_sha` during auto-inference, since comparing a commit to itself provides no meaningful baseline ([#2924](https://github.com/getsentry/sentry-cli/pull/2924)). +- Improved error message when supplying a non-existent organization to `sentry-cli sourcemaps upload`. The error now correctly indicates the organization doesn't exist, rather than incorrectly suggesting the Sentry server lacks artifact bundle support ([#2931](https://github.com/getsentry/sentry-cli/pull/2931)). + +## 2.58.0 + +### New Features + +- Removed experimental status from the `sentry-cli build upload` commands ([#2899](https://github.com/getsentry/sentry-cli/pull/2899), [#2905](https://github.com/getsentry/sentry-cli/pull/2905)). At the time of this release, build uploads are still in closed beta on the server side, so most customers cannot use this functionality quite yet. +- Added CLI version metadata to build upload archives ([#2890](https://github.com/getsentry/sentry-cli/pull/2890)). + +### Deprecations + +- Deprecated the `upload-proguard` subcommand's `--platform` flag ([#2863](https://github.com/getsentry/sentry-cli/pull/2863)). This flag was a no-op for some time, so we will remove it in the next major. +- Deprecated the `upload-proguard` subcommand's `--android-manifest` flag ([#2891](https://github.com/getsentry/sentry-cli/pull/2891)). This flag was a no-op for some time, so we will remove it in the next major. +- Deprecated the `sentry-cli sourcemaps upload` command's `--no-dedupe` flag ([#2913](https://github.com/getsentry/sentry-cli/pull/2913)). The flag was no longer relevant for sourcemap uploads to modern Sentry servers and was made a no-op. + +### Fixes + +- Fixed autofilled git base metadata (`--base-ref`, `--base-sha`) when using the `build upload` subcommand in git repos. Previously this worked only in the context of GitHub workflows ([#2897](https://github.com/getsentry/sentry-cli/pull/2897), [#2898](https://github.com/getsentry/sentry-cli/pull/2898)). + +### Performance + +- Slightly sped up the `sentry-cli sourcemaps upload` command by eliminating an HTTP request to the Sentry server, which was not required in most cases ([#2913](https://github.com/getsentry/sentry-cli/pull/2913)). ## 2.57.0 diff --git a/Cargo.lock b/Cargo.lock index dd0dcdbda2..cb82c0a601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2726,7 +2726,7 @@ dependencies = [ [[package]] name = "sentry-cli" -version = "2.57.0" +version = "2.58.1" dependencies = [ "anyhow", "anylog", diff --git a/Cargo.toml b/Cargo.toml index e679cf6e7f..6cb0b2ac30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Armin Ronacher "] build = "build.rs" name = "sentry-cli" -version = "2.57.0" +version = "2.58.1" edition = "2021" rust-version = "1.86" @@ -99,8 +99,8 @@ with_crash_reporting = [] allow-attributes = "warn" dbg-macro = "warn" elidable-lifetime-names = "warn" +implicit-clone = "warn" str-to-string = "warn" -string-to-string = "warn" tests-outside-test-module = "warn" unnecessary-wraps = "warn" uninlined-format-args = "warn" 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 1faa9c9e75..aa6014fb70 100644 --- a/apple-catalog-parsing/native/swift/AssetCatalogParser/Sources/AssetCatalogParser/AssetCatalogReader.swift +++ b/apple-catalog-parsing/native/swift/AssetCatalogParser/Sources/AssetCatalogParser/AssetCatalogReader.swift @@ -11,7 +11,7 @@ public func swift_inspect_asset_catalog(_ path: UnsafePointer, outputPath let pathString = String(cString: path) let outputPathString = String(cString: outputPath) if #available(macOS 13.0, *) { - let supportedVersions = [13, 14, 15] + let supportedVersions = [13, 14, 15, 26] let version = ProcessInfo.processInfo.operatingSystemVersion if supportedVersions.contains(version.majorVersion) { AssetUtil.disect(file: URL(filePath: pathString), outputURL: URL(filePath: outputPathString)) @@ -38,6 +38,8 @@ struct AssetCatalogEntry: Encodable { let height: Int? let filename: String? let type: AssetType? + let idiom: String? + let colorspace: String? } enum Error: Swift.Error { @@ -48,7 +50,40 @@ typealias objectiveCMethodImp = @convention(c) (AnyObject, Selector, UnsafeRawPo AnyObject >? +private struct MultisizeSetInfo { + let name: String + let element: UInt + let part: UInt + let identifier: UInt + let sizeIndexes: [(idiom: UInt, subtype: UInt)] +} + enum AssetUtil { + private static func idiomToString(_ idiom: UInt?) -> String? { + guard let idiom = idiom else { return nil } + switch idiom { + case 0: return "universal" + case 1: return "phone" + case 2: return "pad" + case 3: return "tv" + case 4: return "carplay" + case 5: return "watch" + case 6: return "marketing" + default: return nil + } + } + + private static func colorSpaceIDToString(_ colorSpaceID: UInt?) -> String? { + guard let colorSpaceID = colorSpaceID else { return nil } + switch colorSpaceID { + case 1: return "srgb" + case 2: return "gray gamma 22" + case 3: return "displayP3" + case 4: return "extended srgb" + default: return nil + } + } + private static func createResultsPath(assetURL: URL, outputURL: URL) throws -> URL { var archiveURL = assetURL var tailComponents: [String] = [] @@ -78,8 +113,12 @@ enum AssetUtil { let (structuredThemeStore, assetKeys) = initializeCatalog(from: file) var images: [String: (cgImage: CGImage, format: String)] = [:] - - for key in assetKeys { + + // First pass: Build map of multisize sets and cache renditions for performance + var multisizeSets: [MultisizeSetInfo] = [] + var renditionCache: [Int: NSObject] = [:] + + for (index, key) in assetKeys.enumerated() { let keyList = unsafeBitCast( key.perform(Selector(("keyList"))), to: UnsafeMutableRawPointer.self @@ -87,6 +126,49 @@ enum AssetUtil { guard let rendition = createRendition(from: structuredThemeStore, keyList) else { continue } + renditionCache[index] = rendition + + let type = rendition.getUInt(forKey: "type") ?? 0 + if type == 1010 { // Multisize image set + let renditionTypeName = rendition.perform(Selector(("name"))).takeUnretainedValue() as! String + let keyElement = key.getUInt(forKey: "themeElement") ?? 0 + let keyPart = key.getUInt(forKey: "themePart") ?? 0 + let keyIdentifier = key.getUInt(forKey: "themeIdentifier") ?? 0 + + // Extract size indexes to identify which images belong to this set + var sizeIndexes: [(idiom: UInt, subtype: UInt)] = [] + if rendition.responds(to: Selector(("sizeIndexes"))), + let sizeIndexesResult = rendition.perform(Selector(("sizeIndexes"))), + let sizeIndexesArray = sizeIndexesResult.takeUnretainedValue() as? NSArray { + for sizeIndexObj in sizeIndexesArray { + if let obj = sizeIndexObj as? NSObject { + let idiom = obj.getUInt(forKey: "idiom") ?? 0 + let subtype = obj.getUInt(forKey: "subtype") ?? 0 + sizeIndexes.append((idiom: idiom, subtype: subtype)) + } + } + } + + multisizeSets.append(MultisizeSetInfo( + name: renditionTypeName, + element: keyElement, + part: keyPart, + identifier: keyIdentifier, + sizeIndexes: sizeIndexes + )) + } + } + + // Second pass: Process all assets using cached renditions + for (index, key) in assetKeys.enumerated() { + guard let rendition = renditionCache[index] else { + continue + } + + let keyList = unsafeBitCast( + key.perform(Selector(("keyList"))), + to: UnsafeMutableRawPointer.self + ) let data = rendition.value(forKey: "_srcData") as! Data let length = UInt(data.count) @@ -126,26 +208,50 @@ enum AssetUtil { let type = rendition.getUInt(forKey: "type") ?? 0 let isVector = type == 9 - let (width, height, unslicedImage) = resolveImageDimensions(rendition, isVector) + let isMultisizeImageSet = type == 1010 let assetType = determineAssetType(key) let imageId = UUID().uuidString - let fileExtension = (renditionTypeName as NSString).pathExtension.lowercased() - // Skip files without an extension or SVGs - if !fileExtension.isEmpty && fileExtension != "svg", let unslicedImage = unslicedImage { - images[imageId] = (cgImage: unslicedImage, format: fileExtension) + var width: Int? + var height: Int? + var unslicedImage: CGImage? + + if isMultisizeImageSet { + continue + } else { + // 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 { + let format = fileExtension.isEmpty ? "png" : fileExtension + images[imageId] = (cgImage: unslicedImage, format: format) + } + } + + let idiomValue = key.getUInt(forKey: "themeIdiom") + let colorSpaceID = rendition.getUInt(forKey: "colorSpaceID") + + // Include multisize set name in the name field if it exists + let finalName: String + if let setName = findMultisizeSetName(key, in: multisizeSets) { + finalName = "\(setName)/\(name)" + } else { + finalName = name } let asset = AssetCatalogEntry( imageId: imageId, size: length, - name: name, + name: finalName, vector: isVector, width: width, height: height, filename: renditionTypeName, - type: assetType + type: assetType, + idiom: idiomToString(idiomValue), + colorspace: colorSpaceIDToString(colorSpaceID) ) assets.append(asset) } @@ -158,7 +264,9 @@ enum AssetUtil { width: nil, height: nil, filename: nil, - type: nil + type: nil, + idiom: nil, + colorspace: nil )) let data = try! JSONEncoder().encode(assets) @@ -254,6 +362,30 @@ enum AssetUtil { } return .image } + + private static func findMultisizeSetName( + _ key: NSObject, + in multisizeSets: [MultisizeSetInfo] + ) -> String? { + let element = key.getUInt(forKey: "themeElement") ?? 0 + let identifier = key.getUInt(forKey: "themeIdentifier") ?? 0 + let idiom = key.getUInt(forKey: "themeIdiom") ?? 0 + let subtype = key.getUInt(forKey: "themeSubtype") ?? 0 + + for setInfo in multisizeSets { + guard setInfo.element == element, + setInfo.identifier == identifier else { + continue + } + + for sizeIndex in setInfo.sizeIndexes { + if sizeIndex.idiom == idiom && sizeIndex.subtype == subtype { + return setInfo.name + } + } + } + return nil + } private static func resolveRenditionName( _ structuredThemeStore: NSObject, diff --git a/js/index.d.ts b/js/index.d.ts index 27a1702814..e0d9f76d5a 100644 --- a/js/index.d.ts +++ b/js/index.d.ts @@ -9,13 +9,15 @@ declare module '@sentry/cli' { */ url?: string; /** - * Authentication token for API, interchangeable with `apiKey`. + * Authentication token for HTTP requests to Sentry. * This value will update `SENTRY_AUTH_TOKEN` env variable. */ authToken?: string; /** - * Authentication token for API, interchangeable with `authToken`. + * 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; /** diff --git a/npm-binary-distributions/darwin/package.json b/npm-binary-distributions/darwin/package.json index 8c462f0e84..aa124838f0 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.57.0", + "version": "2.58.1", "description": "The darwin distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "BSD-3-Clause", diff --git a/npm-binary-distributions/linux-arm/package.json b/npm-binary-distributions/linux-arm/package.json index 02c20a8052..9d37c021c1 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.57.0", + "version": "2.58.1", "description": "The linux arm distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "BSD-3-Clause", diff --git a/npm-binary-distributions/linux-arm64/package.json b/npm-binary-distributions/linux-arm64/package.json index f724036a70..d02fa91394 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.57.0", + "version": "2.58.1", "description": "The linux arm64 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "BSD-3-Clause", diff --git a/npm-binary-distributions/linux-i686/package.json b/npm-binary-distributions/linux-i686/package.json index 0438a59ed4..c4cc16e7b0 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.57.0", + "version": "2.58.1", "description": "The linux x86 and ia32 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "BSD-3-Clause", diff --git a/npm-binary-distributions/linux-x64/package.json b/npm-binary-distributions/linux-x64/package.json index 2a944cf249..24f409d204 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.57.0", + "version": "2.58.1", "description": "The linux x64 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "BSD-3-Clause", diff --git a/npm-binary-distributions/win32-arm64/package.json b/npm-binary-distributions/win32-arm64/package.json index 8669598ca0..ae6cb3ced8 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.57.0", + "version": "2.58.1", "description": "The windows arm64 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "BSD-3-Clause", diff --git a/npm-binary-distributions/win32-i686/package.json b/npm-binary-distributions/win32-i686/package.json index 68b44d360a..bb6495d42b 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.57.0", + "version": "2.58.1", "description": "The windows x86 and ia32 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "BSD-3-Clause", diff --git a/npm-binary-distributions/win32-x64/package.json b/npm-binary-distributions/win32-x64/package.json index 544cf44b56..f18abfedf4 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.57.0", + "version": "2.58.1", "description": "The windows x64 distribution of the Sentry CLI binary.", "repository": "https://github.com/getsentry/sentry-cli", "license": "BSD-3-Clause", diff --git a/package-lock.json b/package-lock.json index 8ff20c45a6..1bc049a437 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli", - "version": "2.57.0", + "version": "2.58.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -767,51 +767,51 @@ } }, "@sentry/cli-darwin": { - "version": "2.56.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.56.1.tgz", - "integrity": "sha512-zfhT8MrvB5x/xRdIVGwg+sG0Cx3i0G6RH2zCrdQ/moWn8TfkwsM0O1k/AxpwbpcRfAHCkVb04CU/yKciKwg2KA==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.0.tgz", + "integrity": "sha512-dI8+85N2xNsQeJZBbfGkjFScYH0xP/8+TDgoA5YiWWxsD/qSlWv1pf2VCR83smMyfcjIkDiPYIxBDticD67skQ==", "optional": true }, "@sentry/cli-linux-arm": { - "version": "2.56.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.56.1.tgz", - "integrity": "sha512-fNB/Ng11HrkGOSEIDg+fc3zfTCV7q6kJddp6ndK3QlYFsCffRSnclaX1SMp+mqxdWkHqe1kkp85OY8G/x5uAWw==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.0.tgz", + "integrity": "sha512-QxBWSQkm2OL8d0XXTUOcX5RYZzZGkMw48ubU4g/c4rlT06PuJV56Z03jsMQdJWUDzKmVYoJdvFV/whxYIkwmWw==", "optional": true }, "@sentry/cli-linux-arm64": { - "version": "2.56.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.56.1.tgz", - "integrity": "sha512-AypXIwZvOMJb9RgjI/98hTAd06FcOjqjIm6G9IR0OI4pJCOcaAXz9NKXdJqxpZd7phSMJnD+Bx/8iYOUPeY73A==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.0.tgz", + "integrity": "sha512-Fso5GImxQOigZqLHAHhz85w71zxS1bvL52PI/tcjadmKrIaJdD3ANukC0UcKyKuj9xhr/k1ufNR7V+2BD16kmg==", "optional": true }, "@sentry/cli-linux-i686": { - "version": "2.56.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.56.1.tgz", - "integrity": "sha512-vnH+WJEsUq7Lf7xc9udzE/M4hoDXXsniFFYr/7BvdnXtCQlNNaWFMXHbEDYAql3baIlHkWoG8cEHWuB/YKyniw==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.0.tgz", + "integrity": "sha512-Av+T5YwuTtbOpe/Fyr/lsbl5XIZTFspHCiAt4Kgtllme6T1ASIDhQDXDh/OVJ8So4pHkToTn3iH8mm8vLqBqOA==", "optional": true }, "@sentry/cli-linux-x64": { - "version": "2.56.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.56.1.tgz", - "integrity": "sha512-3/BlKe5Vdnia36MeovghHJD8lbcum5TFIxLp+PSfH2sVb09+5Jo0L95oRTI2JkD8Fs+QNssvTqTxJj5eIo/n+A==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.0.tgz", + "integrity": "sha512-AxK0eqZbHn0NGWsAE8bzt/iRMMUlqsx77kru/TIBQy9cMMJaq+rLb63W7HWXln4ER32nPZYx+JuhHD9UNiAFHA==", "optional": true }, "@sentry/cli-win32-arm64": { - "version": "2.56.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.56.1.tgz", - "integrity": "sha512-Gg8RV7CV7Tz4fiR1EN1Af5AVhJsnEXiZvfvfQXI4lp51MKAhcxZIMtEfg9HaWsn3Dm/wgwYBinyeywfWbTXYDg==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.0.tgz", + "integrity": "sha512-lIRTfGjD1TQIOuFh4rJGWt3zXyeXAlfoYYQbzG/rP6gXstiGENQtfEXZyKT+wlIGSqtbBGVfL8xp65ryjbXSgQ==", "optional": true }, "@sentry/cli-win32-i686": { - "version": "2.56.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.56.1.tgz", - "integrity": "sha512-6u6a060yC3i76Ze1apqgWr5luQSyhuD5ND84eWfh/UbddsEa42UHjoVHOiBwmpZqf/hvNZAtzLnE4NCvU4zOMg==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.0.tgz", + "integrity": "sha512-7VdB3QZ/3t2FABgIwRP2SoJcDmZaPPPZofVmJem+FgeONeLOUvHQw9WSLG4y5Dfc9yi5wO31H1ClW4uxv8EtuA==", "optional": true }, "@sentry/cli-win32-x64": { - "version": "2.56.1", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.56.1.tgz", - "integrity": "sha512-11cdflajBrDWlRZqI9MOu7ok2vnPzFjKmbU3YvBYWQapNE+HHAsWdsRL/u/P1RmU62vj7Y42iSUcj6x1SNrdPw==", + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.0.tgz", + "integrity": "sha512-uItx4P4v9cKbgVbOpuShvIV8g42qLmZorPHwg3pYUu78c85xAWrmiXL+0JKNUf5JVBEHeHB+rIu08AZfDMhxig==", "optional": true }, "@sinonjs/commons": { diff --git a/package.json b/package.json index ece2653799..dbc11b5c56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cli", - "version": "2.57.0", + "version": "2.58.1", "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,14 +32,14 @@ "typescript": "~5.8.3" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.57.0", - "@sentry/cli-linux-arm": "2.57.0", - "@sentry/cli-linux-arm64": "2.57.0", - "@sentry/cli-linux-i686": "2.57.0", - "@sentry/cli-linux-x64": "2.57.0", - "@sentry/cli-win32-i686": "2.57.0", - "@sentry/cli-win32-x64": "2.57.0", - "@sentry/cli-win32-arm64": "2.57.0" + "@sentry/cli-darwin": "2.58.1", + "@sentry/cli-linux-arm": "2.58.1", + "@sentry/cli-linux-arm64": "2.58.1", + "@sentry/cli-linux-i686": "2.58.1", + "@sentry/cli-linux-x64": "2.58.1", + "@sentry/cli-win32-i686": "2.58.1", + "@sentry/cli-win32-x64": "2.58.1", + "@sentry/cli-win32-arm64": "2.58.1" }, "scripts": { "postinstall": "node ./scripts/install.js", diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d578735b96..77cd27d1b6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] # We pin the minor version to prevent new Clippy lints from breaking CI. # But, we still want to pick up new patch versions. -channel = "1.90" +channel = "1.91" diff --git a/src/api/data_types/chunking/upload/capability.rs b/src/api/data_types/chunking/upload/capability.rs index 729a5e4611..28ab96a249 100644 --- a/src/api/data_types/chunking/upload/capability.rs +++ b/src/api/data_types/chunking/upload/capability.rs @@ -36,6 +36,9 @@ pub enum ChunkUploadCapability { /// Upload of preprod artifacts PreprodArtifacts, + /// Upload of ProGuard mappings + Proguard, + /// Any other unsupported capability (ignored) Unknown, } @@ -57,6 +60,7 @@ impl<'de> Deserialize<'de> for ChunkUploadCapability { "il2cpp" => ChunkUploadCapability::Il2Cpp, "dartsymbolmap" => ChunkUploadCapability::DartSymbolMap, "preprod_artifacts" => ChunkUploadCapability::PreprodArtifacts, + "proguard" => ChunkUploadCapability::Proguard, _ => ChunkUploadCapability::Unknown, }) } diff --git a/src/api/data_types/chunking/upload/options.rs b/src/api/data_types/chunking/upload/options.rs index 5683c9e543..76a1767bca 100644 --- a/src/api/data_types/chunking/upload/options.rs +++ b/src/api/data_types/chunking/upload/options.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroUsize; + use serde::Deserialize; use super::{ChunkCompression, ChunkHashAlgorithm, ChunkUploadCapability}; @@ -17,7 +19,7 @@ pub struct ChunkServerOptions { pub max_wait: u64, #[expect(dead_code)] pub hash_algorithm: ChunkHashAlgorithm, - pub chunk_size: u64, + pub chunk_size: NonZeroUsize, pub concurrency: u8, #[serde(default)] pub compression: Vec, diff --git a/src/api/mod.rs b/src/api/mod.rs index 93c0e0f2b0..b4c63b8255 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1411,6 +1411,7 @@ impl<'a> AuthenticatedApi<'a> { 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." @@ -1771,9 +1772,10 @@ 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 key based authentication"); + debug!("using deprecated key based authentication"); Ok(self) } Auth::Token(ref token) => { diff --git a/src/commands/build/mod.rs b/src/commands/build/mod.rs index f198fcf3dd..72b2216b78 100644 --- a/src/commands/build/mod.rs +++ b/src/commands/build/mod.rs @@ -21,24 +21,16 @@ pub fn make_command(mut command: Command) -> Command { } command = command - .about("[EXPERIMENTAL] Manage builds.") + .about("Manage builds.") .subcommand_required(true) .arg_required_else_help(true) .org_arg() - .project_arg(true) - // TODO: Remove this when ready for release - .hide(true); + .project_arg(true); each_subcommand!(add_subcommand); command } pub fn execute(matches: &ArgMatches) -> Result<()> { - log::warn!( - "EXPERIMENTAL: The build subcommand is experimental. \ - The command is subject to breaking changes and may be removed \ - without notice in any release." - ); - macro_rules! execute_subcommand { ($name:ident) => {{ if let Some(sub_matches) = diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index 2057aca0e6..1f2d9b449f 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -17,7 +17,9 @@ use crate::config::Config; use crate::utils::args::ArgExt as _; #[cfg(all(target_os = "macos", target_arch = "aarch64"))] use crate::utils::build::{handle_asset_catalogs, ipa_to_xcarchive, is_apple_app, is_ipa_file}; -use crate::utils::build::{is_aab_file, is_apk_file, is_zip_file, normalize_directory}; +use crate::utils::build::{ + is_aab_file, is_apk_file, is_zip_file, normalize_directory, write_version_metadata, +}; use crate::utils::chunks::{upload_chunks, Chunk}; use crate::utils::fs::get_sha1_checksums; use crate::utils::fs::TempDir; @@ -37,7 +39,7 @@ pub fn make_command(command: Command) -> Command { const HELP_TEXT: &str = "The path to the build to upload. Supported files include Apk, and Aab."; command - .about("[EXPERIMENTAL] Upload builds to a project.") + .about("Upload builds to a project.") .org_arg() .project_arg(false) .arg( @@ -113,7 +115,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { .get_one("head_sha") .map(String::as_str) .map(Cow::Borrowed) - .or_else(|| vcs::find_head().ok().map(Cow::Owned)); + .or_else(|| vcs::find_head_sha().ok().map(Cow::Owned)); let cached_remote = config.get_cached_vcs_remote(); // Try to open the git repository and find the remote, but handle errors gracefully. @@ -228,17 +230,40 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { base_repo_name, ) }; - let base_sha = matches + + // Track whether base_sha and base_ref were explicitly provided by the user + let base_sha_from_user = matches.get_one::("base_sha").is_some(); + let base_ref_from_user = matches.get_one::("base_ref").is_some(); + + let mut base_sha = matches .get_one("base_sha") .map(String::as_str) .map(Cow::Borrowed) .or_else(|| { - vcs::find_base_sha() + vcs::find_base_sha(&cached_remote) .inspect_err(|e| debug!("Error finding base SHA: {e}")) .ok() .flatten() .map(Cow::Owned) }); + + let mut base_ref = base_ref; + + // If base_sha equals head_sha and both were auto-inferred, skip setting base_sha and base_ref + // but keep head_sha (since comparing a commit to itself provides no meaningful baseline) + if !base_sha_from_user + && !base_ref_from_user + && base_sha.is_some() + && head_sha.is_some() + && base_sha.as_deref() == head_sha.as_deref() + { + debug!( + "Base SHA equals head SHA ({}), and both were auto-inferred. Skipping base_sha and base_ref, but keeping head_sha.", + base_sha.as_deref().expect("base_sha is Some at this point") + ); + base_sha = None; + base_ref = None; + } let pr_number = matches .get_one("pr_number") .copied() @@ -455,6 +480,8 @@ fn normalize_file(path: &Path, bytes: &[u8]) -> Result { zip.start_file(file_name, options)?; zip.write_all(bytes)?; + write_version_metadata(&mut zip)?; + zip.finish()?; debug!("Successfully created normalized zip for file"); Ok(temp_file) @@ -507,10 +534,10 @@ fn upload_file( pb.enable_steady_tick(100); pb.set_style(progress_style); - let chunk_size = chunk_upload_options.chunk_size as usize; - let (checksum, checksums) = get_sha1_checksums(bytes, chunk_size)?; + let chunk_size = chunk_upload_options.chunk_size; + let (checksum, checksums) = get_sha1_checksums(bytes, chunk_size); let mut chunks = bytes - .chunks(chunk_size) + .chunks(chunk_size.into()) .zip(checksums.iter()) .map(|(data, checksum)| Chunk((*checksum, data))) .collect::>(); diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 9786446e0f..b224fee696 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -159,7 +159,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { let options = ChunkOptions::new(chunk_upload_options, org, project) .with_max_wait(DEFAULT_MAX_WAIT); - let chunked = Chunked::from(object, options.server_options().chunk_size as usize)?; + let chunked = Chunked::from(object, options.server_options().chunk_size); let (_uploaded, has_processing_errors) = upload_chunked_objects(&[chunked], options)?; if has_processing_errors { bail!("Some symbol maps did not process correctly"); diff --git a/src/commands/debug_files/bundle_jvm.rs b/src/commands/debug_files/bundle_jvm.rs index 9187884400..884c944e35 100644 --- a/src/commands/debug_files/bundle_jvm.rs +++ b/src/commands/debug_files/bundle_jvm.rs @@ -1,12 +1,11 @@ #![expect(clippy::unwrap_used, reason = "contains legacy code which uses unwrap")] -use crate::api::Api; use crate::config::Config; -use crate::constants::DEFAULT_MAX_WAIT; use crate::utils::args::ArgExt as _; use crate::utils::file_search::ReleaseFileSearch; -use crate::utils::file_upload::{FileUpload, SourceFile, UploadContext}; +use crate::utils::file_upload::SourceFile; use crate::utils::fs::path_as_url; +use crate::utils::source_bundle::{self, BundleContext}; use anyhow::{bail, Context as _, Result}; use clap::{Arg, ArgMatches, Command}; use sentry::types::DebugId; @@ -54,20 +53,8 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let config = Config::current(); let org = config.get_org(matches)?; let project = config.get_project(matches).ok(); - let api = Api::current(); - let chunk_upload_options = api.authenticated()?.get_chunk_upload_options(&org)?; - let context = &UploadContext { - org: &org, - projects: project.as_slice().try_into().ok(), - release: None, - dist: None, - note: None, - wait: true, - max_wait: DEFAULT_MAX_WAIT, - dedupe: false, - chunk_upload_options: chunk_upload_options.as_ref(), - }; + let context = BundleContext::new(&org).with_projects(project.as_slice()); let path = matches.get_one::("path").unwrap(); let output_path = matches.get_one::("output").unwrap(); let debug_id = matches.get_one::("debug_id").unwrap(); @@ -88,31 +75,24 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { ))?; } - let sources = ReleaseFileSearch::new(path.to_path_buf()).collect_files()?; - let files = sources - .iter() - .map(|source| { - let local_path = source.path.strip_prefix(&source.base_path).unwrap(); - let local_path_jvm_ext = local_path.with_extension("jvm"); - let url = format!("~/{}", path_as_url(&local_path_jvm_ext)); - ( - url.clone(), - SourceFile { - url, - path: source.path.clone(), - contents: Arc::new(source.contents.clone()), - ty: SourceFileType::Source, - headers: BTreeMap::new(), - messages: vec![], - already_uploaded: false, - }, - ) - }) - .collect(); + let sources = ReleaseFileSearch::new(path.clone()).collect_files()?; + let files = sources.iter().map(|source| { + let local_path = source.path.strip_prefix(&source.base_path).unwrap(); + let local_path_jvm_ext = local_path.with_extension("jvm"); + let url = format!("~/{}", path_as_url(&local_path_jvm_ext)); - let tempfile = FileUpload::new(context) - .files(&files) - .build_jvm_bundle(Some(*debug_id)) + SourceFile { + url, + path: source.path.clone(), + contents: Arc::new(source.contents.clone()), + ty: SourceFileType::Source, + headers: BTreeMap::new(), + messages: vec![], + already_uploaded: false, + } + }); + + let tempfile = source_bundle::build(context, files, Some(*debug_id)) .context("Unable to create source bundle")?; fs::copy(tempfile.path(), &out).context("Unable to write source bundle")?; diff --git a/src/commands/debug_files/find.rs b/src/commands/debug_files/find.rs index 52843dc856..4cd78c313b 100644 --- a/src/commands/debug_files/find.rs +++ b/src/commands/debug_files/find.rs @@ -324,7 +324,7 @@ where { ids.filter_map(|id| { if remaining.contains(&id) { - return Some((id.to_owned(), t)); + return Some((id, t)); } None }) diff --git a/src/commands/files/upload.rs b/src/commands/files/upload.rs index 04b16c42fb..469211e4f4 100644 --- a/src/commands/files/upload.rs +++ b/src/commands/files/upload.rs @@ -159,7 +159,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { note: None, wait, max_wait, - dedupe: false, chunk_upload_options: chunk_upload_options.as_ref(), }; diff --git a/src/commands/info.rs b/src/commands/info.rs index 0445aeba4c..8ba9f3ef56 100644 --- a/src/commands/info.rs +++ b/src/commands/info.rs @@ -59,7 +59,8 @@ fn describe_auth(auth: Option<&Auth>) -> &str { match auth { None => "Unauthorized", Some(&Auth::Token(_)) => "Auth Token", - Some(&Auth::Key(_)) => "API Key", + #[expect(deprecated, reason = "API key is deprecated.")] + Some(&Auth::Key(_)) => "API Key (deprecated)", } } @@ -74,6 +75,7 @@ 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 = diff --git a/src/commands/login.rs b/src/commands/login.rs index b10658105b..00754e56d1 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -140,6 +140,7 @@ 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 d4f83c6fe7..522c1581b6 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -138,7 +138,15 @@ fn preexecute_hooks() -> Result { fn configure_args(config: &mut Config, matches: &ArgMatches) { if let Some(api_key) = matches.get_one::("api_key") { - config.set_auth(Auth::Key(api_key.to_owned())); + 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") { @@ -188,7 +196,8 @@ fn app() -> Command { Arg::new("api_key") .value_name("API_KEY") .long("api-key") - .help("Use the given Sentry API key."), + .hide(true) + .help("[DEPRECATED] Use the given Sentry API key."), ) .arg( Arg::new("log_level") diff --git a/src/commands/react_native/appcenter.rs b/src/commands/react_native/appcenter.rs index 3b596c80e3..8205fec043 100644 --- a/src/commands/react_native/appcenter.rs +++ b/src/commands/react_native/appcenter.rs @@ -202,7 +202,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { note: None, wait, max_wait, - dedupe: false, chunk_upload_options: chunk_upload_options.as_ref(), })?; } @@ -221,7 +220,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { note: None, wait, max_wait, - dedupe: false, chunk_upload_options: chunk_upload_options.as_ref(), })?; } diff --git a/src/commands/react_native/gradle.rs b/src/commands/react_native/gradle.rs index a9480bfb25..f8573c6ab4 100644 --- a/src/commands/react_native/gradle.rs +++ b/src/commands/react_native/gradle.rs @@ -129,7 +129,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { note: None, wait, max_wait, - dedupe: false, chunk_upload_options: chunk_upload_options.as_ref(), })?; } @@ -143,7 +142,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { note: None, wait, max_wait, - dedupe: false, chunk_upload_options: chunk_upload_options.as_ref(), })?; } diff --git a/src/commands/react_native/xcode.rs b/src/commands/react_native/xcode.rs index e9f8d456d5..a88f23486a 100644 --- a/src/commands/react_native/xcode.rs +++ b/src/commands/react_native/xcode.rs @@ -351,7 +351,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { note: None, wait, max_wait, - dedupe: false, chunk_upload_options: chunk_upload_options.as_ref(), })?; } else { @@ -387,7 +386,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { note: None, wait, max_wait, - dedupe: false, chunk_upload_options: chunk_upload_options.as_ref(), })?; } @@ -401,7 +399,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { note: None, wait, max_wait, - dedupe: false, chunk_upload_options: chunk_upload_options.as_ref(), })?; } diff --git a/src/commands/sourcemaps/upload.rs b/src/commands/sourcemaps/upload.rs index b8a449c81b..ecc98fa30c 100644 --- a/src/commands/sourcemaps/upload.rs +++ b/src/commands/sourcemaps/upload.rs @@ -193,11 +193,8 @@ pub fn make_command(command: Command) -> Command { Arg::new("no_dedupe") .long("no-dedupe") .action(ArgAction::SetTrue) - .help( - "Skip artifacts deduplication prior to uploading. \ - This will force all artifacts to be uploaded, \ - no matter whether they are already present on the server.", - ), + .hide(true) + .help("[DEPRECATED] This flag has no effect and is scheduled for removal."), ) .arg( Arg::new("extensions") @@ -419,6 +416,13 @@ fn process_sources_from_paths( } pub fn execute(matches: &ArgMatches) -> Result<()> { + if matches.get_flag("no_dedupe") { + log::warn!( + "[DEPRECATION NOTICE] The --no-dedupe flag is deprecated and has no \ + effect. It will be removed in the next major version." + ); + } + let config = Config::current(); let version = config.get_release_with_legacy_fallback(matches).ok(); let org = config.get_org(matches)?; @@ -457,7 +461,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { note: matches.get_one::("note").map(String::as_str), wait, max_wait, - dedupe: !matches.get_flag("no_dedupe"), chunk_upload_options: chunk_upload_options.as_ref(), }; diff --git a/src/commands/upload_proguard.rs b/src/commands/upload_proguard.rs index 39d9ee9169..cdb2cb69c2 100644 --- a/src/commands/upload_proguard.rs +++ b/src/commands/upload_proguard.rs @@ -85,12 +85,13 @@ pub fn make_command(command: Command) -> Command { ) .arg( Arg::new("platform") + .hide(true) .long("platform") .value_name("PLATFORM") .requires("app_id") .help( - "Optionally defines the platform for the app association. \ - [defaults to 'android']", + "[DEPRECATED] This flag is a no-op, scheduled \ + for removal in Sentry CLI 3.0.0.", ), ) .arg( @@ -110,7 +111,10 @@ pub fn make_command(command: Command) -> Command { .value_name("PATH") .conflicts_with("app_id") .hide(true) - .help("Read version and version code from an Android manifest file."), + .help( + "[DEPRECATED] This flag is a no-op, scheduled \ + for removal in Sentry CLI 3.0.0.", + ), ) .arg( Arg::new("write_properties") @@ -145,6 +149,22 @@ pub fn make_command(command: Command) -> Command { } pub fn execute(matches: &ArgMatches) -> Result<()> { + if matches.get_one::("platform").is_some() { + log::warn!( + "[DEPRECATION NOTICE] The --platform argument is deprecated, \ + and is scheduled for removal in Sentry CLI 3.0.0. \ + The argument is a no-op." + ); + } + + if matches.get_one::("android_manifest").is_some() { + log::warn!( + "[DEPRECATION NOTICE] The --android-manifest argument is deprecated, \ + and is scheduled for removal in Sentry CLI 3.0.0. \ + The argument is a no-op." + ); + } + let paths: Vec<_> = match matches.get_many::("paths") { Some(paths) => paths.collect(), None => { @@ -194,6 +214,9 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { } } + // We are done constructing the mappings, redeclare as immutable. + let mappings = mappings; + let api = Api::current(); let config = Config::current(); @@ -309,7 +332,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { &org, &project, &AssociateProguard { - release_name: release_name.to_owned(), + release_name: release_name.clone(), proguard_uuid: uuid.to_string(), }, )?; diff --git a/src/config.rs b/src/config.rs index e9477ab739..894c750403 100644 --- a/src/config.rs +++ b/src/config.rs @@ -36,6 +36,7 @@ const MAX_RETRIES_INI_KEY: &str = "max_retries"; /// Represents the auth information #[derive(Debug, Clone)] pub enum Auth { + #[deprecated(note = "Auth Key authentication is deprecated.")] Key(String), Token(AuthToken), } @@ -196,6 +197,7 @@ impl Config { val.raw().expose_secret().clone(), ); } + #[expect(deprecated, reason = "API key is deprecated.")] Some(Auth::Key(ref val)) => { self.ini.set_to(Some("auth"), "api_key".into(), val.clone()); } @@ -734,15 +736,26 @@ impl Clone for Config { } } -#[expect(clippy::manual_map)] fn get_default_auth(ini: &Ini) -> Option { if let Ok(val) = env::var("SENTRY_AUTH_TOKEN") { Some(Auth::Token(val.into())) } else if let Ok(val) = env::var("SENTRY_API_KEY") { + log::warn!( + "[DEPRECTATION NOTICE] API key authentication and the `SENTRY_API_KEY` environment \ + variable are deprecated. \ + Please generate and set an auth token using `SENTRY_AUTH_TOKEN` instead." + ); + #[expect(deprecated, reason = "API key is deprecated.")] Some(Auth::Key(val)) } else if let Some(val) = ini.get_from(Some("auth"), "token") { Some(Auth::Token(val.into())) } else if let Some(val) = ini.get_from(Some("auth"), "api_key") { + log::warn!( + "[DEPRECTATION NOTICE] API key authentication and the `api_key` field in the \ + Sentry CLI config file are deprecated. \ + Please generate and set an auth token instead." + ); + #[expect(deprecated, reason = "API key is deprecated.")] Some(Auth::Key(val.to_owned())) } else { None diff --git a/src/utils/build/mod.rs b/src/utils/build/mod.rs index 819ce2bbde..cb3e2b3197 100644 --- a/src/utils/build/mod.rs +++ b/src/utils/build/mod.rs @@ -5,7 +5,7 @@ mod validation; #[cfg(all(target_os = "macos", target_arch = "aarch64"))] pub use self::apple::{handle_asset_catalogs, ipa_to_xcarchive}; -pub use self::normalize::normalize_directory; +pub use self::normalize::{normalize_directory, write_version_metadata}; pub use self::validation::{is_aab_file, is_apk_file, is_zip_file}; #[cfg(all(target_os = "macos", target_arch = "aarch64"))] pub use self::validation::{is_apple_app, is_ipa_file}; diff --git a/src/utils/build/normalize.rs b/src/utils/build/normalize.rs index c5327b9872..31e8b8531d 100644 --- a/src/utils/build/normalize.rs +++ b/src/utils/build/normalize.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; #[cfg(not(windows))] use std::fs; use std::fs::File; @@ -6,8 +7,9 @@ use std::io::Write as _; use std::os::unix::fs::PermissionsExt as _; use std::path::{Path, PathBuf}; +use crate::constants::VERSION; use crate::utils::fs::TempFile; -use anyhow::Result; +use anyhow::{Context as _, Result}; use itertools::Itertools as _; use log::debug; use symbolic::common::ByteView; @@ -15,6 +17,14 @@ use walkdir::WalkDir; use zip::write::SimpleFileOptions; use zip::{DateTime, ZipWriter}; +fn get_version() -> Cow<'static, str> { + // Integration tests can override the version for consistent test results. + // This ensures deterministic checksums in test fixtures by using a fixed version. + std::env::var("SENTRY_CLI_INTEGRATION_TEST_VERSION_OVERRIDE") + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed(VERSION)) +} + fn sort_entries(path: &Path) -> Result> { Ok(WalkDir::new(path) .into_iter() @@ -61,10 +71,12 @@ fn add_entries_to_zip( let target_str = target.to_string_lossy(); // Create a symlink entry in the zip - zip.add_symlink(zip_path, &target_str, options)?; + zip.add_symlink(zip_path.as_str(), &target_str, options) + .with_context(|| format!("Failed to add symlink '{zip_path}' to zip archive"))?; } else { // Handle regular files - zip.start_file(zip_path, options)?; + zip.start_file(zip_path.as_str(), options) + .with_context(|| format!("Failed to add file '{zip_path}' to zip archive"))?; let file_byteview = ByteView::open(&entry_path)?; zip.write_all(file_byteview.as_slice())?; } @@ -74,6 +86,21 @@ fn add_entries_to_zip( Ok(file_count) } +fn metadata_file_options() -> SimpleFileOptions { + SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated) + .last_modified_time(DateTime::default()) +} + +pub fn write_version_metadata( + zip: &mut ZipWriter, +) -> Result<()> { + let version = get_version(); + zip.start_file(".sentry-cli-metadata.txt", metadata_file_options())?; + writeln!(zip, "sentry-cli-version: {version}")?; + Ok(()) +} + // For XCArchive directories, we'll zip the entire directory // It's important to not change the contents of the directory or the size // analysis will be wrong and the code signature will break. @@ -106,6 +133,8 @@ pub fn normalize_directory(path: &Path, parsed_assets_path: &Path) -> Result { chunks: Vec, /// Size of a single chunk - chunk_size: usize, + chunk_size: NonZeroUsize, } impl Chunked { @@ -63,14 +63,14 @@ where { /// Creates a new `ChunkedObject` from the given object, using /// the given chunk size. - pub fn from(object: T, chunk_size: usize) -> Result { - let (checksum, chunks) = fs::get_sha1_checksums(object.as_ref(), chunk_size)?; - Ok(Self { + pub fn from(object: T, chunk_size: NonZeroUsize) -> Self { + let (checksum, chunks) = fs::get_sha1_checksums(object.as_ref(), chunk_size); + Self { object, checksum, chunks, chunk_size, - }) + } } /// Returns an iterator over all chunks of the object. @@ -79,7 +79,7 @@ where pub fn iter_chunks(&self) -> impl Iterator> { self.object .as_ref() - .chunks(self.chunk_size) + .chunks(self.chunk_size.into()) .zip(self.chunk_hashes().iter()) .map(|(data, checksum)| Chunk((*checksum, data))) } diff --git a/src/utils/dif_upload/mod.rs b/src/utils/dif_upload/mod.rs index 1e28c2e016..e576d3e343 100644 --- a/src/utils/dif_upload/mod.rs +++ b/src/utils/dif_upload/mod.rs @@ -1236,10 +1236,10 @@ fn upload_difs_chunked( processed.extend(source_bundles); } + let chunk_size = chunk_options.chunk_size; + // Calculate checksums and chunks - let chunked = prepare_difs(processed, |m| { - Chunked::from(m, chunk_options.chunk_size as usize) - })?; + let chunked = prepare_difs(processed, |m| Ok(Chunked::from(m, chunk_size)))?; if options.no_upload { println!("{} skipping upload.", style(">").dim()); diff --git a/src/utils/file_upload.rs b/src/utils/file_upload.rs index 402a8e608a..cb837beb3e 100644 --- a/src/utils/file_upload.rs +++ b/src/utils/file_upload.rs @@ -2,7 +2,6 @@ use std::collections::{BTreeMap, HashMap}; use std::ffi::OsStr; use std::fmt::{self, Display}; -use std::io::BufWriter; use std::path::PathBuf; use std::str; use std::sync::Arc; @@ -10,27 +9,23 @@ use std::time::{Duration, Instant}; use anyhow::{anyhow, bail, Result}; use console::style; -use log::info; use parking_lot::RwLock; use rayon::prelude::*; use rayon::ThreadPoolBuilder; -use sentry::types::DebugId; use sha1_smol::Digest; use symbolic::common::ByteView; use symbolic::debuginfo::js; -use symbolic::debuginfo::sourcebundle::{ - SourceBundleErrorKind, SourceBundleWriter, SourceFileInfo, SourceFileType, -}; +use symbolic::debuginfo::sourcebundle::SourceFileType; use thiserror::Error; -use url::Url; use crate::api::NewRelease; use crate::api::{Api, ChunkServerOptions, ChunkUploadCapability}; use crate::constants::DEFAULT_MAX_WAIT; use crate::utils::chunks::{upload_chunks, Chunk, ASSEMBLE_POLL_INTERVAL}; -use crate::utils::fs::{get_sha1_checksum, get_sha1_checksums, TempFile}; +use crate::utils::fs::get_sha1_checksums; use crate::utils::non_empty::NonEmptySlice; use crate::utils::progress::{ProgressBar, ProgressBarMode, ProgressStyle}; +use crate::utils::source_bundle; use super::file_search::ReleaseFileMatch; @@ -80,8 +75,19 @@ pub fn initialize_legacy_release_upload(context: &UploadContext) -> Result<()> { ..Default::default() }, )?; - } else { + } else if context.chunk_upload_options.is_some() { bail!("This version of Sentry does not support artifact bundles. A release slug is required (provide with --release or by setting the SENTRY_RELEASE environment variable)"); + } else { + // We got a 404 when trying to get the chunk options from the server. Most likely, the + // organization does not exist, though old self-hosted Sentry servers may also completely + // lack support for chunked uploads. + bail!( + "The provided organization \"{}\" does not exist. If you are using a self-hosted \ + Sentry server, it is also possible that your Sentry server lacks support for \ + uploading artifact bundles, in which case you need to provide a release slug with \ + --release or by setting the SENTRY_RELEASE environment variable.", + context.org + ); } Ok(()) } @@ -95,7 +101,6 @@ pub struct UploadContext<'a> { pub note: Option<&'a str>, pub wait: bool, pub max_wait: Duration, - pub dedupe: bool, pub chunk_upload_options: Option<&'a ChunkServerOptions>, } @@ -308,11 +313,6 @@ impl SourceFile { source_file } - /// Calculates and returns the SHA1 checksum of the file. - pub fn checksum(&self) -> Result { - get_sha1_checksum(&**self.contents) - } - /// Returns the value of the "debug-id" header. pub fn debug_id(&self) -> Option<&String> { self.headers.get("debug-id") @@ -419,10 +419,6 @@ impl<'a> FileUpload<'a> { #[expect(deprecated, reason = "fallback to legacy upload")] upload_files_parallel(legacy_context, &self.files, concurrency) } - - pub fn build_jvm_bundle(&self, debug_id: Option) -> Result { - build_artifact_bundle(self.context, &self.files, debug_id) - } } #[deprecated = "this non-chunked upload mechanism is deprecated in favor of upload_files_chunked"] @@ -628,7 +624,7 @@ fn upload_files_chunked( files: &SourceFiles, options: &ChunkServerOptions, ) -> Result<()> { - let archive = build_artifact_bundle(context, files, None)?; + let archive = source_bundle::build(context, files.values(), None)?; let progress_style = ProgressStyle::default_spinner().template("{spinner} Optimizing bundle for upload..."); @@ -638,9 +634,9 @@ fn upload_files_chunked( pb.set_style(progress_style); let view = ByteView::open(archive.path())?; - let (checksum, checksums) = get_sha1_checksums(&view, options.chunk_size as usize)?; + let (checksum, checksums) = get_sha1_checksums(&view, options.chunk_size); let mut chunks = view - .chunks(options.chunk_size as usize) + .chunks(options.chunk_size.into()) .zip(checksums.iter()) .map(|(data, checksum)| Chunk((*checksum, data))) .collect::>(); @@ -684,137 +680,6 @@ fn upload_files_chunked( poll_assemble(checksum, &checksums, context, options) } -/// Creates a debug id from a map of source files by hashing each file's -/// URL, contents, type, and headers. -fn build_debug_id(files: &SourceFiles) -> DebugId { - let mut hash = sha1_smol::Sha1::new(); - for source_file in files.values() { - hash.update(source_file.url.as_bytes()); - hash.update(&source_file.contents); - hash.update(format!("{:?}", source_file.ty).as_bytes()); - - for (key, value) in &source_file.headers { - hash.update(key.as_bytes()); - hash.update(value.as_bytes()); - } - } - - let mut sha1_bytes = [0u8; 16]; - sha1_bytes.copy_from_slice(&hash.digest().bytes()[..16]); - DebugId::from_uuid(uuid::Builder::from_sha1_bytes(sha1_bytes).into_uuid()) -} - -fn build_artifact_bundle( - context: &UploadContext, - files: &SourceFiles, - debug_id: Option, -) -> Result { - let progress_style = ProgressStyle::default_bar().template( - "{prefix:.dim} Bundling files for upload... {msg:.dim}\ - \n{wide_bar} {pos}/{len}", - ); - - let pb = ProgressBar::new(files.len()); - pb.set_style(progress_style); - pb.set_prefix(">"); - - let archive = TempFile::create()?; - let mut bundle = SourceBundleWriter::start(BufWriter::new(archive.open()?))?; - - // artifact bundles get a random UUID as debug id - let debug_id = debug_id.unwrap_or_else(|| build_debug_id(files)); - bundle.set_attribute("debug_id", debug_id.to_string()); - - if let Some(note) = context.note { - bundle.set_attribute("note", note.to_owned()); - } - - bundle.set_attribute("org".to_owned(), context.org.to_owned()); - if let Some([project]) = context.projects.as_deref() { - // Only set project if there is exactly one project - bundle.set_attribute("project".to_owned(), project); - } - if let Some(release) = context.release { - bundle.set_attribute("release".to_owned(), release.to_owned()); - } - if let Some(dist) = context.dist { - bundle.set_attribute("dist".to_owned(), dist.to_owned()); - } - - let mut bundle_file_count = 0; - - for file in files.values() { - pb.inc(1); - pb.set_message(&file.url); - - let mut info = SourceFileInfo::new(); - info.set_ty(file.ty); - info.set_url(file.url.clone()); - for (k, v) in &file.headers { - info.add_header(k.clone(), v.clone()); - } - - let bundle_path = url_to_bundle_path(&file.url)?; - if let Err(e) = bundle.add_file(bundle_path, file.contents.as_slice(), info) { - if e.kind() == SourceBundleErrorKind::ReadFailed { - info!( - "Skipping {} because it is not valid UTF-8.", - file.path.display() - ); - continue; - } else { - return Err(e.into()); - } - } - bundle_file_count += 1; - } - - bundle.finish()?; - - pb.finish_with_duration("Bundling"); - - println!( - "{} Bundled {} {} for upload", - style(">").dim(), - style(bundle_file_count).yellow(), - match bundle_file_count { - 1 => "file", - _ => "files", - } - ); - - println!( - "{} Bundle ID: {}", - style(">").dim(), - style(debug_id).yellow(), - ); - - Ok(archive) -} - -fn url_to_bundle_path(url: &str) -> Result { - let base = Url::parse("http://~").expect("this url is valid"); - let url = if let Some(rest) = url.strip_prefix("~/") { - base.join(rest)? - } else { - base.join(url)? - }; - - let mut path = url.path().to_owned(); - if let Some(fragment) = url.fragment() { - path = format!("{path}#{fragment}"); - } - if path.starts_with('/') { - path.remove(0); - } - - Ok(match url.host_str() { - Some("~") => format!("_/_/{path}"), - Some(host) => format!("{}/{host}/{path}", url.scheme()), - None => format!("{}/_/{path}", url.scheme()), - }) -} - fn print_upload_context_details(context: &UploadContext) { println!( "{} {}", @@ -859,78 +724,3 @@ fn is_hermes_bytecode(slice: &[u8]) -> bool { const HERMES_MAGIC: [u8; 8] = [0xC6, 0x1F, 0xBC, 0x03, 0xC1, 0x03, 0x19, 0x1F]; slice.starts_with(&HERMES_MAGIC) } - -#[cfg(test)] -mod tests { - use sha1_smol::Sha1; - - use super::*; - - #[test] - fn test_url_to_bundle_path() { - assert_eq!(url_to_bundle_path("~/bar").unwrap(), "_/_/bar"); - assert_eq!(url_to_bundle_path("~/foo/bar").unwrap(), "_/_/foo/bar"); - assert_eq!( - url_to_bundle_path("~/dist/js/bundle.js.map").unwrap(), - "_/_/dist/js/bundle.js.map" - ); - assert_eq!( - url_to_bundle_path("~/babel.config.js").unwrap(), - "_/_/babel.config.js" - ); - - assert_eq!(url_to_bundle_path("~/#/bar").unwrap(), "_/_/#/bar"); - assert_eq!(url_to_bundle_path("~/foo/#/bar").unwrap(), "_/_/foo/#/bar"); - assert_eq!( - url_to_bundle_path("~/dist/#js/bundle.js.map").unwrap(), - "_/_/dist/#js/bundle.js.map" - ); - assert_eq!( - url_to_bundle_path("~/#foo/babel.config.js").unwrap(), - "_/_/#foo/babel.config.js" - ); - } - - #[test] - fn build_artifact_bundle_deterministic() { - let projects_slice = &["wat-project".into()]; - let context = UploadContext { - org: "wat-org", - projects: Some(projects_slice.into()), - release: None, - dist: None, - note: None, - wait: false, - max_wait: DEFAULT_MAX_WAIT, - dedupe: true, - chunk_upload_options: None, - }; - - let source_files = ["bundle.min.js.map", "vendor.min.js.map"] - .into_iter() - .map(|name| { - let file = SourceFile { - url: format!("~/{name}"), - path: format!("tests/integration/_fixtures/{name}").into(), - contents: std::fs::read(format!("tests/integration/_fixtures/{name}")) - .unwrap() - .into(), - ty: SourceFileType::SourceMap, - headers: Default::default(), - messages: Default::default(), - already_uploaded: false, - }; - (format!("~/{name}"), file) - }) - .collect(); - - let file = build_artifact_bundle(&context, &source_files, None).unwrap(); - - let buf = std::fs::read(file.path()).unwrap(); - let hash = Sha1::from(buf); - assert_eq!( - hash.digest().to_string(), - "f0e25ae149b711c510148e022ebc883ad62c7c4c" - ); - } -} diff --git a/src/utils/fs.rs b/src/utils/fs.rs index b296dcd2d7..3849afa75e 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -2,9 +2,10 @@ use std::env; use std::fs; use std::io; use std::io::{Read, Seek}; +use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; -use anyhow::{bail, Result}; +use anyhow::Result; use flate2::read::GzDecoder; use log::error; use sha1_smol::{Digest, Sha1}; @@ -159,22 +160,18 @@ pub fn get_sha1_checksum(rdr: R) -> Result { /// Returns the SHA1 hash for the entire input, as well as each chunk of it. The /// `chunk_size` must be non-zero. -pub fn get_sha1_checksums(data: &[u8], chunk_size: usize) -> Result<(Digest, Vec)> { - if chunk_size == 0 { - bail!("Chunk size may not be zero."); - } - +pub fn get_sha1_checksums(data: &[u8], chunk_size: NonZeroUsize) -> (Digest, Vec) { let mut total_sha = Sha1::new(); let mut chunks = Vec::new(); - for chunk in data.chunks(chunk_size) { + for chunk in data.chunks(chunk_size.into()) { let mut chunk_sha = Sha1::new(); chunk_sha.update(chunk); total_sha.update(chunk); chunks.push(chunk_sha.digest()); } - Ok((total_sha.digest(), chunks)) + (total_sha.digest(), chunks) } /// Checks if provided slice contains gzipped data. @@ -245,9 +242,10 @@ mod tests { #[test] fn sha1_checksums_power_of_two() { + const NONZERO_16: NonZeroUsize = NonZeroUsize::new(16).unwrap(); + let data = b"this is some binary data for the test"; - let (total_sha, chunks) = - get_sha1_checksums(data, 16).expect("Method should not fail because 16 is not zero"); + let (total_sha, chunks) = get_sha1_checksums(data, NONZERO_16); assert_eq!( total_sha.to_string(), @@ -268,10 +266,11 @@ mod tests { #[test] fn sha1_checksums_not_power_of_two() { + const NONZERO_17: NonZeroUsize = NonZeroUsize::new(17).unwrap(); + let data = b"this is some binary data for the test"; - let (total_sha, chunks) = - get_sha1_checksums(data, 17).expect("Method should not fail because 17 is not zero"); + let (total_sha, chunks) = get_sha1_checksums(data, NONZERO_17); assert_eq!( total_sha.to_string(), @@ -289,10 +288,4 @@ mod tests { ] ); } - - #[test] - fn sha1_checksums_zero() { - let data = b"this is some binary data for the test"; - get_sha1_checksums(data, 0).expect_err("Method should fail because 0 is zero"); - } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f35b872039..54bdc992e1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -21,6 +21,7 @@ pub mod progress; pub mod proguard; pub mod releases; pub mod retry; +pub mod source_bundle; pub mod sourcemaps; pub mod system; pub mod ui; diff --git a/src/utils/proguard/upload.rs b/src/utils/proguard/upload.rs index 94a20a254f..f232eb83b2 100644 --- a/src/utils/proguard/upload.rs +++ b/src/utils/proguard/upload.rs @@ -26,10 +26,11 @@ pub fn chunk_upload( org: &str, project: &str, ) -> Result<()> { + let chunk_size = chunk_upload_options.chunk_size; let chunked_mappings = mappings .iter() - .map(|mapping| Chunked::from(mapping, chunk_upload_options.chunk_size as usize)) - .collect::>>()?; + .map(|mapping| Chunked::from(mapping, chunk_size)) + .collect::>(); let options = ChunkOptions::new(chunk_upload_options, org, project).with_max_wait(ASSEMBLE_POLL_TIMEOUT); diff --git a/src/utils/releases.rs b/src/utils/releases.rs index a37b97a69f..6d062ab9d2 100644 --- a/src/utils/releases.rs +++ b/src/utils/releases.rs @@ -158,7 +158,8 @@ pub fn detect_release_name() -> Result { } } - match vcs::find_head() { + // finally try the git sha + match vcs::find_head_sha() { Ok(head) => Ok(head), Err(e) => Err(anyhow!( "Could not automatically determine release name:\n\t {e} \n\n\ diff --git a/src/utils/source_bundle.rs b/src/utils/source_bundle.rs new file mode 100644 index 0000000000..461ad5097a --- /dev/null +++ b/src/utils/source_bundle.rs @@ -0,0 +1,265 @@ +use std::borrow::Borrow; +use std::io::BufWriter; + +use anyhow::Result; +use indicatif::ProgressStyle; +use sentry::types::DebugId; +use symbolic::debuginfo::sourcebundle::{ + SourceBundleErrorKind, SourceBundleWriter, SourceFileInfo, +}; +use url::Url; + +use crate::utils::file_upload::{SourceFile, UploadContext}; +use crate::utils::fs::TempFile; +use crate::utils::non_empty::NonEmptySlice; +use crate::utils::progress::ProgressBar; + +#[derive(Clone, Copy, Debug, Default)] +pub struct BundleContext<'a> { + org: &'a str, + projects: Option>, + note: Option<&'a str>, + release: Option<&'a str>, + dist: Option<&'a str>, +} + +impl<'a> BundleContext<'a> { + /// Make a new context with the given organization; other fields + /// are left to None. + pub fn new(org: &'a str) -> Self { + Self { + org, + ..Default::default() + } + } + + pub fn with_projects(mut self, projects: &'a [String]) -> Self { + self.projects = projects.try_into().ok(); + self + } +} + +impl<'a> From<&'a UploadContext<'a>> for BundleContext<'a> { + fn from(context: &'a UploadContext<'a>) -> Self { + Self { + org: context.org, + projects: context.projects, + note: context.note, + release: context.release, + dist: context.dist, + } + } +} + +/// Builds a source bundle from a list of source files, setting the metadata +/// from the upload context. +/// +/// Returns a `TempFile` containing the source bundle. +pub fn build<'a, C, F, S>(context: C, files: F, debug_id: Option) -> Result +where + C: Into>, + F: IntoIterator, + S: Borrow, +{ + let context = context.into(); + let files = files.into_iter().collect::>(); + + let progress_style = ProgressStyle::default_bar().template( + "{prefix:.dim} Bundling files for upload... {msg:.dim}\ + \n{wide_bar} {pos}/{len}", + ); + + let pb = ProgressBar::new(files.len()); + pb.set_style(progress_style); + pb.set_prefix(">"); + + let archive = TempFile::create()?; + let mut bundle = SourceBundleWriter::start(BufWriter::new(archive.open()?))?; + + // source bundles get a random UUID as debug id + let debug_id = debug_id.unwrap_or_else(|| build_debug_id(&files)); + bundle.set_attribute("debug_id", debug_id.to_string()); + + if let Some(note) = context.note { + bundle.set_attribute("note", note.to_owned()); + } + + bundle.set_attribute("org".to_owned(), context.org.to_owned()); + if let Some([project]) = context.projects.as_deref() { + // Only set project if there is exactly one project + bundle.set_attribute("project".to_owned(), project); + } + if let Some(release) = context.release { + bundle.set_attribute("release".to_owned(), release.to_owned()); + } + if let Some(dist) = context.dist { + bundle.set_attribute("dist".to_owned(), dist.to_owned()); + } + + let mut bundle_file_count = 0; + + for file in files.iter().map(Borrow::borrow) { + pb.inc(1); + pb.set_message(&file.url); + + let mut info = SourceFileInfo::new(); + info.set_ty(file.ty); + info.set_url(file.url.clone()); + for (k, v) in &file.headers { + info.add_header(k.clone(), v.clone()); + } + + let bundle_path = url_to_bundle_path(&file.url)?; + if let Err(e) = bundle.add_file(bundle_path, file.contents.as_slice(), info) { + if e.kind() == SourceBundleErrorKind::ReadFailed { + log::info!( + "Skipping {} because it is not valid UTF-8.", + file.path.display() + ); + continue; + } else { + return Err(e.into()); + } + } + bundle_file_count += 1; + } + + bundle.finish()?; + + pb.finish_with_duration("Bundling"); + + println!( + "{} Bundled {} {} for upload", + console::style(">").dim(), + console::style(bundle_file_count).yellow(), + match bundle_file_count { + 1 => "file", + _ => "files", + } + ); + + println!( + "{} Bundle ID: {}", + console::style(">").dim(), + console::style(debug_id).yellow(), + ); + + Ok(archive) +} + +/// Creates a debug id from a map of source files by hashing each file's +/// URL, contents, type, and headers. +fn build_debug_id(files: &[S]) -> DebugId +where + S: Borrow, +{ + let mut hash = sha1_smol::Sha1::new(); + for source_file in files.iter().map(Borrow::borrow) { + hash.update(source_file.url.as_bytes()); + hash.update(&source_file.contents); + hash.update(format!("{:?}", source_file.ty).as_bytes()); + + for (key, value) in &source_file.headers { + hash.update(key.as_bytes()); + hash.update(value.as_bytes()); + } + } + + let mut sha1_bytes = [0u8; 16]; + sha1_bytes.copy_from_slice(&hash.digest().bytes()[..16]); + DebugId::from_uuid(uuid::Builder::from_sha1_bytes(sha1_bytes).into_uuid()) +} + +fn url_to_bundle_path(url: &str) -> Result { + let base = Url::parse("http://~").expect("this url is valid"); + let url = if let Some(rest) = url.strip_prefix("~/") { + base.join(rest)? + } else { + base.join(url)? + }; + + let mut path = url.path().to_owned(); + if let Some(fragment) = url.fragment() { + path = format!("{path}#{fragment}"); + } + if path.starts_with('/') { + path.remove(0); + } + + Ok(match url.host_str() { + Some("~") => format!("_/_/{path}"), + Some(host) => format!("{}/{host}/{path}", url.scheme()), + None => format!("{}/_/{path}", url.scheme()), + }) +} + +#[cfg(test)] +mod tests { + use sha1_smol::Sha1; + use symbolic::debuginfo::sourcebundle::SourceFileType; + + use crate::utils::file_upload::SourceFile; + + use super::*; + + #[test] + fn test_url_to_bundle_path() { + assert_eq!(url_to_bundle_path("~/bar").unwrap(), "_/_/bar"); + assert_eq!(url_to_bundle_path("~/foo/bar").unwrap(), "_/_/foo/bar"); + assert_eq!( + url_to_bundle_path("~/dist/js/bundle.js.map").unwrap(), + "_/_/dist/js/bundle.js.map" + ); + assert_eq!( + url_to_bundle_path("~/babel.config.js").unwrap(), + "_/_/babel.config.js" + ); + + assert_eq!(url_to_bundle_path("~/#/bar").unwrap(), "_/_/#/bar"); + assert_eq!(url_to_bundle_path("~/foo/#/bar").unwrap(), "_/_/foo/#/bar"); + assert_eq!( + url_to_bundle_path("~/dist/#js/bundle.js.map").unwrap(), + "_/_/dist/#js/bundle.js.map" + ); + assert_eq!( + url_to_bundle_path("~/#foo/babel.config.js").unwrap(), + "_/_/#foo/babel.config.js" + ); + } + + #[test] + fn build_deterministic() { + let projects_slice = &["wat-project".into()]; + let context = BundleContext { + org: "wat-org", + projects: Some(projects_slice.into()), + release: None, + dist: None, + note: None, + }; + + let source_files = ["bundle.min.js.map", "vendor.min.js.map"] + .into_iter() + .map(|name| SourceFile { + url: format!("~/{name}"), + path: format!("tests/integration/_fixtures/{name}").into(), + contents: std::fs::read(format!("tests/integration/_fixtures/{name}")) + .unwrap() + .into(), + ty: SourceFileType::SourceMap, + headers: Default::default(), + messages: Default::default(), + already_uploaded: false, + }) + .collect::>(); + + let file = build(context, &source_files, None).unwrap(); + + let buf = std::fs::read(file.path()).unwrap(); + let hash = Sha1::from(buf); + assert_eq!( + hash.digest().to_string(), + "f0e25ae149b711c510148e022ebc883ad62c7c4c" + ); + } +} diff --git a/src/utils/sourcemaps.rs b/src/utils/sourcemaps.rs index 293ed69443..974893f836 100644 --- a/src/utils/sourcemaps.rs +++ b/src/utils/sourcemaps.rs @@ -6,7 +6,6 @@ use std::io::Write as _; use std::mem; use std::path::{Path, PathBuf}; use std::str; -use std::str::FromStr as _; use std::sync::Arc; use anyhow::{anyhow, bail, Context as _, Error, Result}; @@ -14,13 +13,11 @@ use console::style; use indicatif::ProgressStyle; use log::{debug, info, warn}; use sentry::types::DebugId; -use sha1_smol::Digest; use sourcemap::{DecodedMap, SourceMap}; use symbolic::debuginfo::js::discover_sourcemaps_location; use symbolic::debuginfo::sourcebundle::SourceFileType; use url::Url; -use crate::api::Api; use crate::utils::file_search::ReleaseFileMatch; use crate::utils::file_upload::{ initialize_legacy_release_upload, FileUpload, SourceFile, SourceFiles, UploadContext, @@ -666,54 +663,6 @@ impl SourceMapProcessor { } } - /// Flags the collected sources whether they have already been uploaded before - /// (based on their checksum), and returns the number of files that *do* need an upload. - fn flag_uploaded_sources(&mut self, context: &UploadContext<'_>) -> usize { - let mut files_needing_upload = self.sources.len(); - - if !context.dedupe { - return files_needing_upload; - } - - // This endpoint only supports at most one project, and a release is required. - // If the upload contains multiple projects or no release, we do not use deduplication. - let (project, release) = match (context.projects.as_deref(), context.release) { - (Some([project]), Some(release)) => (Some(project.as_str()), release), - (None, Some(release)) => (None, release), - _ => return files_needing_upload, - }; - - let mut sources_checksums: Vec<_> = self - .sources - .values() - .filter_map(|s| s.checksum().map(|c| c.to_string()).ok()) - .collect(); - - // Checksums need to be sorted in order to satisfy integration tests constraints. - sources_checksums.sort(); - - let api = Api::current(); - - if let Ok(artifacts) = api.authenticated().and_then(|api| { - api.list_release_files_by_checksum(context.org, project, release, &sources_checksums) - }) { - let already_uploaded_checksums: HashSet<_> = artifacts - .into_iter() - .filter_map(|artifact| Digest::from_str(&artifact.sha1).ok()) - .collect(); - - for source in self.sources.values_mut() { - if let Ok(checksum) = source.checksum() { - if already_uploaded_checksums.contains(&checksum) { - source.already_uploaded = true; - files_needing_upload -= 1; - } - } - } - } - files_needing_upload - } - /// Uploads all files, and on success, returns the number of files that were /// uploaded, wrapped in Ok() pub fn upload(&mut self, context: &UploadContext<'_>) -> Result { @@ -752,7 +701,7 @@ impl SourceMapProcessor { } } } - let files_needing_upload = self.flag_uploaded_sources(context); + let files_needing_upload = self.sources.len(); if files_needing_upload > 0 { let mut uploader = FileUpload::new(context); uploader.files(&self.sources); diff --git a/src/utils/vcs.rs b/src/utils/vcs.rs index a8c042250b..f745e02b24 100644 --- a/src/utils/vcs.rs +++ b/src/utils/vcs.rs @@ -272,30 +272,26 @@ pub fn git_repo_head_ref(repo: &git2::Repository) -> Result { } pub fn git_repo_base_ref(repo: &git2::Repository, remote_name: &str) -> Result { - // Get the current HEAD commit - let head_commit = repo.head()?.peel_to_commit()?; - - // Try to find the remote tracking branch let remote_branch_name = format!("refs/remotes/{remote_name}/HEAD"); let remote_ref = repo.find_reference(&remote_branch_name).map_err(|e| { anyhow::anyhow!("Could not find remote tracking branch for {remote_name}: {e}") })?; - find_merge_base_ref(repo, &head_commit, &remote_ref) -} + let name = remote_ref + .resolve()? + .shorthand() + .ok_or(anyhow::anyhow!("Remote branch name is not valid UTF-8"))? + .to_owned(); -fn find_merge_base_ref( - repo: &git2::Repository, - head_commit: &git2::Commit, - remote_ref: &git2::Reference, -) -> Result { - let remote_commit = remote_ref.peel_to_commit()?; - let merge_base_oid = repo.merge_base(head_commit.id(), remote_commit.id())?; - - // Return the merge-base commit SHA as the base reference - let merge_base_sha = merge_base_oid.to_string(); - debug!("Found merge-base commit as base reference: {merge_base_sha}"); - Ok(merge_base_sha) + let expected_prefix = format!("{remote_name}/"); + name.strip_prefix(&expected_prefix) + .map(|s| s.to_owned()) + .ok_or_else(|| { + anyhow::anyhow!( + "Remote branch name '{name}' does not start with expected prefix \ + '{expected_prefix}'" + ) + }) } /// Like git_repo_base_repo_name but preserves the original case of the repository name. @@ -554,7 +550,7 @@ fn find_matching_revs( Ok((prev_rev, rev)) } -pub fn find_head() -> Result { +pub fn find_head_sha() -> Result { if let Some(pr_head_sha) = std::env::var("GITHUB_EVENT_PATH") .ok() .and_then(|event_path| std::fs::read_to_string(event_path).ok()) @@ -569,23 +565,41 @@ pub fn find_head() -> Result { Ok(head.id().to_string()) } -pub fn find_base_sha() -> Result> { - let github_event = std::env::var("GITHUB_EVENT_PATH") - .map_err(Error::from) - .and_then(|event_path| std::fs::read_to_string(event_path).map_err(Error::from)) - .context("Failed to read GitHub event path")?; +pub fn find_base_sha(remote_name: &str) -> Result> { + if let Some(pr_base_sha) = std::env::var("GITHUB_EVENT_PATH") + .ok() + .and_then(|event_path| std::fs::read_to_string(event_path).ok()) + .and_then(|content| extract_pr_base_sha_from_event(&content)) + { + debug!("Using GitHub Actions PR base SHA from event payload: {pr_base_sha}"); + return Ok(Some(pr_base_sha)); + } + + let repo = git2::Repository::open_from_env().context("Could not open repository")?; + + let head_commit = repo.head()?.peel_to_commit()?; - extract_pr_base_sha_from_event(&github_event) + let remote_branch_name = format!("refs/remotes/{remote_name}/HEAD"); + let remote_ref = repo + .find_reference(&remote_branch_name) + .with_context(|| "Could not find default branch for {remote_name}")?; + + Ok(remote_ref + .peel_to_commit() + .and_then(|remote_commit| repo.merge_base(head_commit.id(), remote_commit.id())) + .map(|oid| oid.to_string()) + .ok() + .inspect(|sha| debug!("Found merge-base commit as base reference: {sha}"))) } /// Extracts the PR head SHA from GitHub Actions event payload JSON. /// Returns None if not a PR event or if SHA cannot be extracted. +/// Panics if json is malformed. fn extract_pr_head_sha_from_event(json_content: &str) -> Option { let v: Value = match serde_json::from_str(json_content) { Ok(v) => v, Err(_) => { - debug!("Failed to parse GitHub event payload as JSON"); - return None; + panic!("Failed to parse GitHub event payload as JSON"); } }; @@ -595,15 +609,19 @@ fn extract_pr_head_sha_from_event(json_content: &str) -> Option { } /// Extracts the PR base SHA from GitHub Actions event payload JSON. -/// Returns Ok(None) if not a PR event or if SHA cannot be extracted. -/// Returns an error if we cannot parse the JSON. -fn extract_pr_base_sha_from_event(json_content: &str) -> Result> { - let v: Value = serde_json::from_str(json_content) - .context("Failed to parse GitHub event payload as JSON")?; +/// Returns None if not a PR event or if SHA cannot be extracted. +/// Panics if json is malformed. +fn extract_pr_base_sha_from_event(json_content: &str) -> Option { + let v: Value = match serde_json::from_str(json_content) { + Ok(v) => v, + Err(_) => { + panic!("Failed to parse GitHub event payload as JSON"); + } + }; - Ok(v.pointer("/pull_request/base/sha") + v.pointer("/pull_request/base/sha") .and_then(|s| s.as_str()) - .map(|s| s.to_owned())) + .map(|s| s.to_owned()) } /// Given commit specs, repos and remote_name this returns a list of head @@ -1602,7 +1620,6 @@ mod tests { } } }"#; - assert_eq!(extract_pr_head_sha_from_event(malformed_json), None); assert_eq!(extract_pr_head_sha_from_event("{}"), None); @@ -1655,8 +1672,19 @@ mod tests { extract_pr_head_sha_from_event(any_sha_json), Some("invalid-sha-123".to_owned()) ); + } - assert_eq!(extract_pr_head_sha_from_event("invalid json {"), None); + #[test] + #[serial(github_env)] + #[should_panic] + fn test_extract_pr_base_sha_from_event_invalid() { + extract_pr_base_sha_from_event("invalid json {"); + } + #[test] + #[serial(github_env)] + #[should_panic] + fn test_extract_pr_head_sha_from_event_invalid() { + extract_pr_head_sha_from_event("invalid json {"); } #[test] @@ -1678,7 +1706,7 @@ mod tests { fs::write(&event_file, pr_json).expect("Failed to write event file"); std::env::set_var("GITHUB_EVENT_PATH", event_file.to_str().unwrap()); - let result = find_head(); + let result = find_head_sha(); std::env::remove_var("GITHUB_EVENT_PATH"); assert!(result.is_ok()); @@ -1705,7 +1733,7 @@ mod tests { .to_string(); assert_eq!( - extract_pr_base_sha_from_event(&pr_json).unwrap(), + extract_pr_base_sha_from_event(&pr_json), Some("55e6bc8c264ce95164314275d805f477650c440d".to_owned()) ); @@ -1719,10 +1747,7 @@ mod tests { }) .to_string(); - assert_eq!(extract_pr_base_sha_from_event(&push_json).unwrap(), None); - - // Test with malformed JSON - assert!(extract_pr_base_sha_from_event("invalid json {").is_err()); + assert_eq!(extract_pr_base_sha_from_event(&push_json), None); // Test with missing base SHA let incomplete_json = r#"{ @@ -1733,10 +1758,7 @@ mod tests { } }"#; - assert_eq!( - extract_pr_base_sha_from_event(incomplete_json).unwrap(), - None - ); + assert_eq!(extract_pr_base_sha_from_event(incomplete_json), None); } #[test] @@ -1765,7 +1787,7 @@ mod tests { fs::write(&event_file, pr_json).expect("Failed to write event file"); std::env::set_var("GITHUB_EVENT_PATH", event_file.to_str().unwrap()); - let result = find_base_sha(); + let result = find_base_sha("origin"); assert_eq!( result.unwrap().unwrap(), "55e6bc8c264ce95164314275d805f477650c440d" @@ -1773,7 +1795,7 @@ mod tests { // Test without GITHUB_EVENT_PATH std::env::remove_var("GITHUB_EVENT_PATH"); - let result = find_base_sha(); + let result = find_base_sha("no_such_remote"); assert!(result.is_err()); } } diff --git a/src/utils/xcode.rs b/src/utils/xcode.rs index 2d50f60a4b..6c62ce5131 100644 --- a/src/utils/xcode.rs +++ b/src/utils/xcode.rs @@ -93,7 +93,7 @@ fn get_xcode_project_info(path: &Path) -> Result> { for entry in (fs::read_dir(path)?).flatten() { if let Some(filename) = entry.file_name().to_str() { if filename.ends_with(".xcodeproj") { - projects.push(entry.path().to_path_buf()); + projects.push(entry.path().clone()); } } } diff --git a/tests/integration/_cases/build/build-help.trycmd b/tests/integration/_cases/build/build-help.trycmd index 8ae4f3a6f4..ca926a2b6b 100644 --- a/tests/integration/_cases/build/build-help.trycmd +++ b/tests/integration/_cases/build/build-help.trycmd @@ -1,12 +1,12 @@ ``` $ sentry-cli build --help ? success -[EXPERIMENTAL] Manage builds. +Manage builds. Usage: sentry-cli[EXE] build [OPTIONS] Commands: - upload [EXPERIMENTAL] Upload builds to a project. + upload Upload builds to a project. help Print this message or the help of the given subcommand(s) Options: diff --git a/tests/integration/_cases/build/build-upload-apk-all-uploaded.trycmd b/tests/integration/_cases/build/build-upload-apk-all-uploaded.trycmd index 9ed3adbd1a..7b60447042 100644 --- a/tests/integration/_cases/build/build-upload-apk-all-uploaded.trycmd +++ b/tests/integration/_cases/build/build-upload-apk-all-uploaded.trycmd @@ -1,7 +1,6 @@ ``` $ sentry-cli build upload tests/integration/_fixtures/build/apk.apk ? success - WARN [..] EXPERIMENTAL: The build subcommand is experimental. The command is subject to breaking changes and may be removed without notice in any release. > Preparing for upload completed in [..] Successfully uploaded 1 file to Sentry - tests/integration/_fixtures/build/apk.apk (http[..]/wat-org/preprod/wat-project/42) diff --git a/tests/integration/_cases/build/build-upload-apk-no-token.trycmd b/tests/integration/_cases/build/build-upload-apk-no-token.trycmd index 4f4c4c3576..b77a5e68aa 100644 --- a/tests/integration/_cases/build/build-upload-apk-no-token.trycmd +++ b/tests/integration/_cases/build/build-upload-apk-no-token.trycmd @@ -1,7 +1,6 @@ ``` $ sentry-cli build upload tests/integration/_fixtures/build/apk.apk ? failed - WARN [..] EXPERIMENTAL: The build subcommand is experimental. The command is subject to breaking changes and may be removed without notice in any release. error: Auth token is required for this request. Please run `sentry-cli login` and try again! Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output. diff --git a/tests/integration/_cases/build/build-upload-apk.trycmd b/tests/integration/_cases/build/build-upload-apk.trycmd index aec1d188f9..a31bfc1bd1 100644 --- a/tests/integration/_cases/build/build-upload-apk.trycmd +++ b/tests/integration/_cases/build/build-upload-apk.trycmd @@ -1,7 +1,6 @@ ``` $ sentry-cli build upload tests/integration/_fixtures/build/apk.apk --head-sha test_head_sha ? success - WARN [..] EXPERIMENTAL: The build subcommand is experimental. The command is subject to breaking changes and may be removed without notice in any release. > Preparing for upload completed in [..] > Uploading completed in [..] Successfully uploaded 1 file to Sentry diff --git a/tests/integration/_cases/build/build-upload-help-macos.trycmd b/tests/integration/_cases/build/build-upload-help-macos.trycmd index b5f0729c66..0411e4dc2a 100644 --- a/tests/integration/_cases/build/build-upload-help-macos.trycmd +++ b/tests/integration/_cases/build/build-upload-help-macos.trycmd @@ -1,7 +1,7 @@ ``` $ sentry-cli build upload --help ? success -[EXPERIMENTAL] Upload builds to a project. +Upload builds to a project. Usage: sentry-cli[EXE] build upload [OPTIONS] ... diff --git a/tests/integration/_cases/build/build-upload-help-not-macos.trycmd b/tests/integration/_cases/build/build-upload-help-not-macos.trycmd index 2f17b5e004..0415496ac0 100644 --- a/tests/integration/_cases/build/build-upload-help-not-macos.trycmd +++ b/tests/integration/_cases/build/build-upload-help-not-macos.trycmd @@ -1,6 +1,6 @@ ``` $ sentry-cli build upload --help -[EXPERIMENTAL] Upload builds to a project. +Upload builds to a project. Usage: sentry-cli[EXE] build upload [OPTIONS] ... diff --git a/tests/integration/_cases/build/build-upload-ipa.trycmd b/tests/integration/_cases/build/build-upload-ipa.trycmd index 747423f16d..73b6656e52 100644 --- a/tests/integration/_cases/build/build-upload-ipa.trycmd +++ b/tests/integration/_cases/build/build-upload-ipa.trycmd @@ -1,7 +1,6 @@ ``` $ sentry-cli build upload tests/integration/_fixtures/build/ipa.ipa --head-sha test_head_sha ? success - WARN [..] EXPERIMENTAL: The build subcommand is experimental. The command is subject to breaking changes and may be removed without notice in any release. > Preparing for upload completed in [..] Successfully uploaded 1 file to Sentry - tests/integration/_fixtures/build/ipa.ipa (http[..]/wat-org/preprod/wat-project/some-text-id) diff --git a/tests/integration/_cases/help/help-windows.trycmd b/tests/integration/_cases/help/help-windows.trycmd index c367a53c11..7b98475db6 100644 --- a/tests/integration/_cases/help/help-windows.trycmd +++ b/tests/integration/_cases/help/help-windows.trycmd @@ -11,6 +11,7 @@ Usage: sentry-cli[EXE] [OPTIONS] Commands: completions Generate completions for the specified shell. + build Manage builds. debug-files Locate, analyze or upload debug information files. [aliases: dif] deploys Manage deployments for Sentry releases. events Manage events on Sentry. @@ -37,7 +38,6 @@ Options: --header Custom headers that should be attached to all requests in key:value format. --auth-token Use the given Sentry auth token. - --api-key Use the given Sentry API key. --log-level Set the log output verbosity. [possible values: trace, debug, info, warn, error] --quiet Do not print any output while preserving correct exit code. This diff --git a/tests/integration/_cases/help/help.trycmd b/tests/integration/_cases/help/help.trycmd index a7fcba86c7..5061a03fc8 100644 --- a/tests/integration/_cases/help/help.trycmd +++ b/tests/integration/_cases/help/help.trycmd @@ -11,6 +11,7 @@ Usage: sentry-cli[EXE] [OPTIONS] Commands: completions Generate completions for the specified shell. + build Manage builds. debug-files Locate, analyze or upload debug information files. [aliases: dif] deploys Manage deployments for Sentry releases. events Manage events on Sentry. @@ -38,7 +39,6 @@ Options: --header Custom headers that should be attached to all requests in key:value format. --auth-token Use the given Sentry auth token. - --api-key Use the given Sentry API key. --log-level Set the log output verbosity. [possible values: trace, debug, info, warn, error] --quiet Do not print any output while preserving correct exit code. This diff --git a/tests/integration/_cases/sourcemaps/sourcemaps-upload-help.trycmd b/tests/integration/_cases/sourcemaps/sourcemaps-upload-help.trycmd index 31b6336332..63c096ef92 100644 --- a/tests/integration/_cases/sourcemaps/sourcemaps-upload-help.trycmd +++ b/tests/integration/_cases/sourcemaps/sourcemaps-upload-help.trycmd @@ -74,9 +74,6 @@ Options: Path to the application bundle (indexed, file, or regular) --bundle-sourcemap Path to the bundle sourcemap - --no-dedupe - Skip artifacts deduplication prior to uploading. This will force all artifacts to be - uploaded, no matter whether they are already present on the server. -x, --ext 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 diff --git a/tests/integration/_cases/sourcemaps/sourcemaps-upload-no-dedupe.trycmd b/tests/integration/_cases/sourcemaps/sourcemaps-upload-no-dedupe.trycmd index 35b00a50c3..ee633b6ae8 100644 --- a/tests/integration/_cases/sourcemaps/sourcemaps-upload-no-dedupe.trycmd +++ b/tests/integration/_cases/sourcemaps/sourcemaps-upload-no-dedupe.trycmd @@ -1,6 +1,7 @@ ``` $ sentry-cli sourcemaps upload tests/integration/_fixtures/bundle.min.js.map tests/integration/_fixtures/vendor.min.js.map --release=wat-release --no-dedupe ? success + WARN [..] [DEPRECATION NOTICE] The --no-dedupe flag is deprecated and has no effect. It will be removed in the next major version. > Found 1 file > Found 1 file > Analyzing 2 sources diff --git a/tests/integration/_cases/sourcemaps/sourcemaps-upload-skip-already-uploaded.trycmd b/tests/integration/_cases/sourcemaps/sourcemaps-upload-skip-already-uploaded.trycmd index 9eb2e6072e..1e25abe4c1 100644 --- a/tests/integration/_cases/sourcemaps/sourcemaps-upload-skip-already-uploaded.trycmd +++ b/tests/integration/_cases/sourcemaps/sourcemaps-upload-skip-already-uploaded.trycmd @@ -9,7 +9,7 @@ $ sentry-cli sourcemaps upload tests/integration/_fixtures/bundle.min.js.map tes > Rewriting completed in [..] > Adding source map references > Bundling completed in [..] -> Bundled 1 file for upload +> Bundled 2 files for upload > Bundle ID: [..]-[..]-[..]-[..]-[..] > Optimizing completed in [..] > Uploading completed in [..] @@ -26,6 +26,6 @@ $ sentry-cli sourcemaps upload tests/integration/_fixtures/bundle.min.js.map tes Source Map Upload Report Source Maps ~/bundle.min.js.map - ~/vendor.min.js.map (skipped; already uploaded) + ~/vendor.min.js.map ``` diff --git a/tests/integration/_cases/upload_proguard/upload_proguard-help.trycmd b/tests/integration/_cases/upload_proguard/upload_proguard-help.trycmd index 63e24850a9..5d2a42cc6d 100644 --- a/tests/integration/_cases/upload_proguard/upload_proguard-help.trycmd +++ b/tests/integration/_cases/upload_proguard/upload_proguard-help.trycmd @@ -16,8 +16,6 @@ Options: --auth-token Use the given Sentry auth token. --log-level Set the log output verbosity. [possible values: trace, debug, info, warn, error] - --platform Optionally defines the platform for the app association. [defaults - to 'android'] --quiet Do not print any output while preserving correct exit code. This flag is currently implemented only for selected subcommands. [aliases: --silent] diff --git a/tests/integration/_expected_requests/build/apk_chunk.bin b/tests/integration/_expected_requests/build/apk_chunk.bin index 27f4a850b0..7e2b601683 100644 Binary files a/tests/integration/_expected_requests/build/apk_chunk.bin and b/tests/integration/_expected_requests/build/apk_chunk.bin differ diff --git a/tests/integration/build/upload.rs b/tests/integration/build/upload.rs index 93d5ca9ddd..17f3642974 100644 --- a/tests/integration/build/upload.rs +++ b/tests/integration/build/upload.rs @@ -162,7 +162,7 @@ fn command_build_upload_apk_chunked() { if is_first_assemble_call.swap(false, Ordering::Relaxed) { r#"{ "state": "created", - "missingChunks": ["18e40e6e932d0b622d631e887be454cc2003dbb5"] + "missingChunks": ["60863d91bb673a1b1b92dbbe91b1de5cc0dde146"] }"# } else { r#"{ @@ -176,6 +176,9 @@ fn command_build_upload_apk_chunked() { .expect(2), ) .register_trycmd_test("build/build-upload-apk.trycmd") + // We override the version in the metadata field to ensure a consistent checksum + // for the uploaded files. + .env("SENTRY_CLI_INTEGRATION_TEST_VERSION_OVERRIDE", "0.0.0-test") .with_default_token(); } diff --git a/tests/integration/sourcemaps/upload.rs b/tests/integration/sourcemaps/upload.rs index e1b0306726..f8abce16ac 100644 --- a/tests/integration/sourcemaps/upload.rs +++ b/tests/integration/sourcemaps/upload.rs @@ -14,13 +14,6 @@ fn command_sourcemaps_upload() { fn command_sourcemaps_upload_successfully_upload_file() { TestManager::new() .mock_common_upload_endpoints(ServerBehavior::Legacy, Default::default()) - .mock_endpoint( - MockEndpointBuilder::new( - "GET", - "/api/0/projects/wat-org/wat-project/releases/wat-release/files/?cursor=&checksum=38ed853073df85147960ea3a5bced6170ec389b0", - ) - .with_response_body("[]"), - ) .register_trycmd_test("sourcemaps/sourcemaps-upload-successfully-upload-file.trycmd") .with_default_token() .assert_mock_endpoints(); @@ -30,22 +23,6 @@ fn command_sourcemaps_upload_successfully_upload_file() { fn command_sourcemaps_upload_skip_already_uploaded() { TestManager::new() .mock_common_upload_endpoints(ServerBehavior::Legacy, Default::default()) - .mock_endpoint( - MockEndpointBuilder::new( - "GET", - "/api/0/projects/wat-org/wat-project/releases/wat-release/files/?cursor=&checksum=38ed853073df85147960ea3a5bced6170ec389b0&checksum=f3673e2cea68bcb86bb74254a9efaa381d74929f", - ) - .with_response_body( - r#"[{ - "id": "1337", - "name": "~/vendor.min.js.map", - "headers": {}, - "size": 1522, - "sha1": "f3673e2cea68bcb86bb74254a9efaa381d74929f", - "dateCreated": "2022-05-12T11:08:01.496220Z" - }]"#, - ), - ) .register_trycmd_test("sourcemaps/sourcemaps-upload-skip-already-uploaded.trycmd") .with_default_token() .assert_mock_endpoints();