Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## unreleased

### Added

- Add `--exclude` option to all upload commands to exclude files matching specific patterns. Supports wildcards like `*.map` for file extensions, exact filenames, and substring matching for paths (e.g., `node_modules`, `/dist/`). [#269](https://github.com/bugsnag/bugsnag-cli/pull/269)

### Changed

- Configure HTTP client to use HTTP/1.1 instead of HTTP/2 for all upload and build API requests. [#270](https://github.com/bugsnag/bugsnag-cli/pull/270)

## [3.8.0] - 2026-03-03

### Changed
Expand Down
35 changes: 35 additions & 0 deletions features/cli/exclude-option.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Feature: Exclude option tests

Scenario: Exclude files with wildcard extension pattern
When I run bugsnag-cli with upload js --upload-api-root-url=http://localhost:$MAZE_RUNNER_PORT --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --base-url=example.com --exclude=*.map --project-root=features/js/fixtures/js-multiple-maps features/js/fixtures/js-multiple-maps/dist/
Then I should receive no sourcemaps

Scenario: Exclude files matching specific filename pattern
When I run bugsnag-cli with upload js --upload-api-root-url=http://localhost:$MAZE_RUNNER_PORT --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --base-url=example.com --exclude=main.js.map --project-root=features/js/fixtures/js-multiple-maps features/js/fixtures/js-multiple-maps/dist/
And I wait to receive 1 sourcemap
Then the sourcemaps are valid for the API
And the sourcemap payload field "minifiedUrl" equals "example.com/other.js"

Scenario: Exclude files with multiple patterns
When I run bugsnag-cli with upload js --upload-api-root-url=http://localhost:$MAZE_RUNNER_PORT --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --base-url=example.com --exclude=main.js.map --exclude=other.js.map --project-root=features/js/fixtures/js-multiple-maps features/js/fixtures/js-multiple-maps/dist/
Then I should receive no sourcemaps

Scenario: Exclude with path pattern
When I run bugsnag-cli with upload js --upload-api-root-url=http://localhost:$MAZE_RUNNER_PORT --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --base-url=example.com --exclude=features/js/fixtures/js-multiple-maps/dist/** --project-root=features/js/fixtures/js-multiple-maps features/js/fixtures/js-multiple-maps/dist/
Then I should receive no sourcemaps

Scenario: Exclude with absolute path
Given I get the current working directory
When I run bugsnag-cli with upload js --upload-api-root-url=http://localhost:$MAZE_RUNNER_PORT --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --base-url=example.com --exclude=$ABS_PATH/features/js/fixtures/js-multiple-maps/dist/other.js.map --project-root=features/js/fixtures/js-multiple-maps features/js/fixtures/js-multiple-maps/dist/
And I wait to receive 1 sourcemap
Then the sourcemaps are valid for the API
And the sourcemap payload field "minifiedUrl" equals "example.com/main.js"

Scenario: Exclude with path glob pattern
When I run bugsnag-cli with upload js --upload-api-root-url=http://localhost:$MAZE_RUNNER_PORT --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --base-url=example.com --exclude=**/dist/** --project-root=features/js/fixtures/js-multiple-maps features/js/fixtures/js-multiple-maps/dist/
Then I should receive no sourcemaps

Scenario: Upload succeeds when exclude pattern doesn't match
When I run bugsnag-cli with upload js --upload-api-root-url=http://localhost:$MAZE_RUNNER_PORT --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --base-url=example.com --exclude=*.log --project-root=features/js/fixtures/js-multiple-maps features/js/fixtures/js-multiple-maps/dist/
And I wait to receive 2 sourcemaps
Then the sourcemaps are valid for the API
5 changes: 5 additions & 0 deletions features/steps/steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,8 @@ def clean_and_build(scheme, project_path, build_target)
Dir.chdir(base_dir)
Maze.check.include(`ls #{@fixture_dir}/dist`, 'index.js.map')
end

Given('I get the current working directory') do
@current_dir = Dir.pwd
ENV['ABS_PATH'] = @current_dir
end
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
module github.com/bugsnag/bugsnag-cli

go 1.23.1
go 1.26.1

require (
github.com/alecthomas/kong v0.7.1
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/mattn/go-isatty v0.0.20
github.com/mitchellh/mapstructure v1.5.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
golang.org/x/text v0.14.0
google.golang.org/protobuf v1.34.2
howett.net/plist v1.0.1
)

require (
Expand All @@ -21,5 +23,4 @@ require (
golang.org/x/sys v0.19.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4
github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand All @@ -19,8 +21,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
Expand Down
2 changes: 1 addition & 1 deletion pkg/android/process-uploads.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func UploadAndroidNdk(
"/ndk-symbol",
params,
fileField,
filepath.Base(originalFile),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this change not have a functional effect? Or does it fix an existing limitation for nested files?

originalFile,
opts,
logger,
)
Expand Down
20 changes: 18 additions & 2 deletions pkg/ios/process-dsym.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,28 @@ func ProcessDsymUpload(plistPath string, projectRoot string, options options.CLI
}

// Attempt to upload the dSYM file.
err = server.ProcessFileRequest(options.ApiKey, "/dsym", uploadOptions, fileFieldData, dsym.UUID, options, logger)
err = server.ProcessFileRequest(
options.ApiKey,
"/dsym",
uploadOptions,
fileFieldData,
dsym.UUID,
options,
logger,
)
if err != nil {
// Retry with the base endpoint if a 404 error occurs.
if strings.Contains(err.Error(), "404 Not Found") {
logger.Debug(fmt.Sprintf("Retrying upload for dSYM %s at base endpoint", dsymInfo))
err = server.ProcessFileRequest(options.ApiKey, "", uploadOptions, fileFieldData, dsym.UUID, options, logger)
err = server.ProcessFileRequest(
options.ApiKey,
"",
uploadOptions,
fileFieldData,
dsym.UUID,
options,
logger,
)
}
if err != nil {
return fmt.Errorf("failed to upload dSYM %s: %w", dsymInfo, err)
Expand Down
8 changes: 4 additions & 4 deletions pkg/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ type Breakpad struct {

type Upload struct {
// shared options
Retries int `help:"The number of retry attempts before failing an upload request" default:"0"`
Timeout int `help:"The number of seconds to wait before failing an upload request" default:"300"`
UploadAPIRootUrl string `help:"The upload server hostname, optionally containing port number"`

Retries int `help:"The number of retry attempts before failing an upload request" default:"0"`
Timeout int `help:"The number of seconds to wait before failing an upload request" default:"300"`
UploadAPIRootUrl string `help:"The upload server hostname, optionally containing port number"`
Exclude []string `help:"Exclude files matching these patterns. Supports wildcards (*.map), recursive globs (node_modules/**, **/*.test.js) and exact filenames (file.js.map)."`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Exclude []string `help:"Exclude files matching these patterns. Supports wildcards (*.map), recursive globs (node_modules/**, **/*.test.js) and exact filenames (file.js.map)."`
Exclude []string `help:"Exclude files matching these patterns. Supports wildcards (*.map), recursive globs (node_modules/**, **/*.test.js) and exact filenames (file.js.map). Non-absolute path patterns are relative to the current directory."`

// required options
All DiscoverAndUploadAny `cmd:"" help:"Upload any symbol/mapping files"`
AndroidAab AndroidAabMapping `cmd:"" help:"Process and upload application bundle files for Android"`
Expand Down
22 changes: 20 additions & 2 deletions pkg/server/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package server
import (
"bytes"
"fmt"
"github.com/bugsnag/bugsnag-cli/pkg/endpoints"
"io"
"mime/multipart"
"net/http"
Expand All @@ -14,6 +13,7 @@ import (

"github.com/pkg/errors"

"github.com/bugsnag/bugsnag-cli/pkg/endpoints"
"github.com/bugsnag/bugsnag-cli/pkg/log"
"github.com/bugsnag/bugsnag-cli/pkg/options"
"github.com/bugsnag/bugsnag-cli/pkg/utils"
Expand Down Expand Up @@ -121,6 +121,14 @@ func buildFileRequest(url string, fieldData map[string]string, fileFieldData map
// - error: An error if any step of the file processing fails. Nil if the process is successful.
func ProcessFileRequest(apiKey string, endpointPath string, uploadOptions map[string]string, fileFieldData map[string]FileField, fileName string, options options.CLI, logger log.Logger) error {

// Check if the fileName itself should be excluded based on exclude patterns
if len(options.Upload.Exclude) > 0 {
if utils.IsFileExcluded(fileName, options.Upload.Exclude) {
logger.Info(fmt.Sprintf("Skipping the upload of: %s (matches exclude pattern)", fileName))
return nil
}
}

if apiKey != "" {
uploadOptions["apiKey"] = apiKey
} else {
Expand Down Expand Up @@ -249,8 +257,18 @@ func processRequest(request *http.Request, timeout int, retryCount int, logger l
// Returns:
// - error: An error if any step of the request processing fails. Nil if the process is successful.
func sendRequest(request *http.Request, timeout int, logger log.Logger) error {
// Configure transport to use HTTP/1.1 only
var protocols http.Protocols
protocols.SetHTTP1(true)
protocols.SetHTTP2(false)

transport := &http.Transport{
Protocols: &protocols,
}

client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Timeout: time.Duration(timeout) * time.Second,
Transport: transport,
}

response, err := client.Do(request)
Expand Down
10 changes: 9 additions & 1 deletion pkg/upload/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,15 @@ func All(options options.CLI, logger log.Logger) error {
fileFieldData["file"] = server.LocalFile(file)
}

err := server.ProcessFileRequest(options.ApiKey, "", uploadOptions, fileFieldData, file, options, logger)
err := server.ProcessFileRequest(
options.ApiKey,
"",
uploadOptions,
fileFieldData,
file,
options,
logger,
)
if err != nil {
return err
}
Expand Down
20 changes: 18 additions & 2 deletions pkg/upload/android-proguard.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,28 @@ func ProcessAndroidProguard(options options.CLI, logger log.Logger) error {
fileFieldData["proguard"] = server.LocalFile(outputFile)

// Attempt upload to Bugsnag API
err = server.ProcessFileRequest(options.ApiKey, "/proguard", uploadOptions, fileFieldData, outputFile, options, logger)
err = server.ProcessFileRequest(
options.ApiKey,
"/proguard",
uploadOptions,
fileFieldData,
outputFile,
options,
logger,
)

// Retry at base endpoint if 404 received
if err != nil && strings.Contains(err.Error(), "404 Not Found") {
logger.Debug("Retrying upload for proguard at base endpoint")
err = server.ProcessFileRequest(options.ApiKey, "", uploadOptions, fileFieldData, outputFile, options, logger)
err = server.ProcessFileRequest(
options.ApiKey,
"",
uploadOptions,
fileFieldData,
outputFile,
options,
logger,
)
}

if err != nil {
Expand Down
10 changes: 9 additions & 1 deletion pkg/upload/breakpad.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,15 @@ func ProcessBreakpad(globalOptions options.CLI, logger log.Logger) error {
)

// Send the file upload request to the Breakpad symbol endpoint
err = server.ProcessFileRequest(apiKey, "/breakpad-symbol"+queryParams, formFields, fileFieldData, file, globalOptions, logger)
err = server.ProcessFileRequest(
apiKey,
"/breakpad-symbol"+queryParams,
formFields,
fileFieldData,
file,
globalOptions,
logger,
)
if err != nil {
return err
}
Expand Down
20 changes: 18 additions & 2 deletions pkg/upload/dart.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,15 @@ func Dart(options options.CLI, logger log.Logger) error {
fileFieldData := make(map[string]server.FileField)
fileFieldData["symbolFile"] = server.LocalFile(file)

err := server.ProcessFileRequest(options.ApiKey, "/dart-symbol", uploadOptions, fileFieldData, file, options, logger)
err := server.ProcessFileRequest(
options.ApiKey,
"/dart-symbol",
uploadOptions,
fileFieldData,
file,
options,
logger,
)

if err != nil {

Expand Down Expand Up @@ -91,7 +99,15 @@ func Dart(options options.CLI, logger log.Logger) error {
if options.DryRun {
err = nil
} else {
err = server.ProcessFileRequest(options.ApiKey, "/dart-symbol", uploadOptions, fileFieldData, file, options, logger)
err = server.ProcessFileRequest(
options.ApiKey,
"/dart-symbol",
uploadOptions,
fileFieldData,
file,
options,
logger,
)
}

if err != nil {
Expand Down
10 changes: 9 additions & 1 deletion pkg/upload/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,15 @@ func uploadSingleSourceMap(sourceMapPath string, bundlePath string, bundleUrl st
fileFieldData["sourceMap"] = sourceMapFile
fileFieldData["minifiedFile"] = server.LocalFile(bundlePath)

err = server.ProcessFileRequest(options.ApiKey, "/sourcemap", uploadOptions, fileFieldData, sourceMapPath, options, logger)
err = server.ProcessFileRequest(
options.ApiKey,
"/sourcemap",
uploadOptions,
fileFieldData,
sourceMapPath,
options,
logger,
)

if err != nil {
return fmt.Errorf("encountered error when uploading js sourcemap: %s", err.Error())
Expand Down
2 changes: 1 addition & 1 deletion pkg/upload/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func uploadSymbolFile(symbolFile string, linuxOpts options.LinuxOptions, opts op
"/linux",
uploadOpts,
fileField,
filepath.Base(symbolFile),
symbolFile,
opts,
logger,
); err != nil {
Expand Down
37 changes: 37 additions & 0 deletions pkg/utils/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"path/filepath"
"strings"
"time"

"github.com/bmatcuk/doublestar/v4"
)

const (
Expand Down Expand Up @@ -51,6 +53,41 @@ func IsDir(path string) bool {
return err == nil && pathInfo.IsDir()
}

// IsFileExcluded checks if a file path matches any of the exclude patterns.
// It supports wildcards including *.map, path/to/*, and recursive patterns like node_modules/**.
//
// Parameters:
// - filePath: The file path to check.
// - excludePatterns: A list of patterns to match against (supports ** for recursive matching).
//
// Returns:
// - bool: True if the file matches any exclude pattern, false otherwise.
func IsFileExcluded(filePath string, excludePatterns []string) bool {
for _, pattern := range excludePatterns {
// Try matching the pattern against the full path using doublestar (supports **)
matched, err := doublestar.Match(pattern, filePath)
if err == nil && matched {
return true
}

// If pattern doesn't start with **, also try matching with **/ prepended
// This allows patterns like "node_modules/**" to match anywhere in the path
if !strings.HasPrefix(pattern, "**/") {
matched, err = doublestar.Match("**/"+pattern, filePath)
if err == nil && matched {
return true
}
}

// Try matching against the base name
matched, err = doublestar.Match(pattern, filepath.Base(filePath))
if err == nil && matched {
return true
}
}
return false
}

// BuildFileList compiles a list of files from the provided paths.
//
// Parameters:
Expand Down
Loading