Skip to content
Merged
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
12 changes: 10 additions & 2 deletions .github/workflows/go-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
os: [macos-latest, ubuntu-latest]
steps:
- name: Set up Go 1.24.6
uses: actions/setup-go@v1
uses: actions/setup-go@v6
with:
go-version: 1.24.6
id: go
Expand All @@ -28,6 +28,10 @@ jobs:
- name: Run Bitbucket Cloud Integration Tests
run: scripts/bitbucket_cloud_integration_tests.sh
env:
# New API Token authentication (recommended)
BITBUCKET_API_TOKEN: ${{ secrets.GHORG_BITBUCKET_API_TOKEN }}
BITBUCKET_API_EMAIL: ${{ secrets.GHORG_BITBUCKET_API_EMAIL }}
# Legacy App Password authentication (for backward compatibility testing)
BITBUCKET_TOKEN: ${{ secrets.GHORG_BITBUCKET_APP_PASSWORD }}
BITBUCKET_USERNAME: ${{ secrets.GHORG_BITBUCKET_USERNAME }}
- name: Run GitLab Cloud Integration Tests
Expand All @@ -46,7 +50,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Set up Go 1.24.6
uses: actions/setup-go@v1
uses: actions/setup-go@v6
with:
go-version: 1.24.6
id: go
Expand All @@ -67,6 +71,10 @@ jobs:
- name: Run Bitbucket Integration Tests
run: scripts/bitbucket_cloud_integration_tests.sh
env:
# New API Token authentication (recommended)
BITBUCKET_API_TOKEN: ${{ secrets.GHORG_BITBUCKET_API_TOKEN }}
BITBUCKET_API_EMAIL: ${{ secrets.GHORG_BITBUCKET_API_EMAIL }}
# Legacy App Password authentication (for backward compatibility testing)
BITBUCKET_TOKEN: ${{ secrets.GHORG_BITBUCKET_APP_PASSWORD }}
BITBUCKET_USERNAME: ${{ secrets.GHORG_BITBUCKET_USERNAME }}
- name: Run GitLab Cloud Integration Tests Linux
Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,23 @@ Note: ghorg will respect the `XDG_CONFIG_HOME` [environment variable](https://wi

> Note: ghorg supports both Bitbucket Cloud and Bitbucket Server (self-hosted instances)

#### App Passwords
#### API Tokens (Recommended for Bitbucket Cloud)

1. To configure with bitbucket you will need to create a new [app password](https://confluence.atlassian.com/bitbucket/app-passwords-828781300.html) and update your `$HOME/.config/ghorg/conf.yaml` or use the (--token, -t) and (--bitbucket-username) flags.
1. Update [SCM type](https://github.com/gabrie30/ghorg/blob/master/sample-conf.yaml#L54-L57) to `bitbucket` in your `ghorg/conf.yaml` or via cli flags
Bitbucket has deprecated App Passwords in favor of API Tokens. This is the recommended authentication method for Bitbucket Cloud.

1. Create an [API token](https://support.atlassian.com/bitbucket-cloud/docs/create-an-api-token/) from your Atlassian account settings
1. **Important**: When creating the token, grant **all read scopes** (Account: Read, Workspace membership: Read, Projects: Read, Repositories: Read, etc.) to ensure ghorg can list and clone repositories
1. Set `GHORG_BITBUCKET_API_TOKEN` in your `$HOME/.config/ghorg/conf.yaml` or use the `--token` flag
1. Set `GHORG_BITBUCKET_API_EMAIL` to your Atlassian account email (or use `--bitbucket-api-email`)
1. Update SCM type to `bitbucket` in your `ghorg/conf.yaml` or via cli flags
1. See [examples/bitbucket.md](https://github.com/gabrie30/ghorg/blob/master/examples/bitbucket.md) on how to run

> Note: When using API tokens, ghorg automatically uses `x-bitbucket-api-token-auth` as the Git username for clone operations, as required by Bitbucket's API token authentication.

#### App Passwords (Legacy)

> Note: Bitbucket has deprecated App Passwords. Consider using API Tokens instead.

#### PAT/OAuth token

1. Create a [PAT](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html)
Expand Down
21 changes: 19 additions & 2 deletions cmd/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ func cloneFunc(cmd *cobra.Command, argz []string) {
os.Setenv("GHORG_BITBUCKET_USERNAME", cmd.Flag("bitbucket-username").Value.String())
}

if cmd.Flags().Changed("bitbucket-api-email") {
os.Setenv("GHORG_BITBUCKET_API_EMAIL", cmd.Flag("bitbucket-api-email").Value.String())
}

if cmd.Flags().Changed("clone-type") {
cloneType := strings.ToLower(cmd.Flag("clone-type").Value.String())
os.Setenv("GHORG_CLONE_TYPE", cloneType)
Expand Down Expand Up @@ -331,10 +335,23 @@ func cloneFunc(cmd *cobra.Command, argz []string) {
} else if os.Getenv("GHORG_SCM_TYPE") == "gitlab" {
os.Setenv("GHORG_GITLAB_TOKEN", token)
} else if os.Getenv("GHORG_SCM_TYPE") == "bitbucket" {
if cmd.Flags().Changed("bitbucket-username") {
// Auto-detect token type based on configuration:
// 1. If GHORG_BITBUCKET_API_EMAIL is set, treat as API token
// 2. If --bitbucket-username is provided, treat as app password (legacy)
// 3. If GHORG_BITBUCKET_USERNAME is set but no API email, treat as app password (legacy)
// 4. Otherwise, treat as API token (new default for Bitbucket Cloud)
if os.Getenv("GHORG_BITBUCKET_API_EMAIL") != "" {
// API email explicitly set - use API token auth
os.Setenv("GHORG_BITBUCKET_API_TOKEN", cmd.Flag("token").Value.String())
} else if cmd.Flags().Changed("bitbucket-username") {
// Username provided via flag - use app password (legacy)
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", cmd.Flag("token").Value.String())
} else if os.Getenv("GHORG_BITBUCKET_USERNAME") != "" && os.Getenv("GHORG_BITBUCKET_API_TOKEN") == "" {
// Username set in config but no API token - assume app password for backward compat
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", cmd.Flag("token").Value.String())
} else {
os.Setenv("GHORG_BITBUCKET_OAUTH_TOKEN", cmd.Flag("token").Value.String())
// Default to API token for new Bitbucket Cloud authentication
os.Setenv("GHORG_BITBUCKET_API_TOKEN", cmd.Flag("token").Value.String())
}
} else if os.Getenv("GHORG_SCM_TYPE") == "gitea" {
os.Setenv("GHORG_GITEA_TOKEN", token)
Expand Down
33 changes: 33 additions & 0 deletions cmd/examples-copy/bitbucket.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,45 @@ To view all additional flags see the [sample-conf.yaml](https://github.com/gabri

## Bitbucket Cloud

### API Token Authentication (Recommended)

Bitbucket has deprecated App Passwords in favor of API Tokens. This is the recommended authentication method.

**Creating the API Token:**
1. Go to your [Atlassian account settings](https://id.atlassian.com/manage/api-tokens)
2. Create a new API token
3. **Important**: Grant **all read scopes** (Account: Read, Workspace membership: Read, Projects: Read, Repositories: Read) to ensure ghorg can list and clone repositories

**Using the API Token:**

1. Clone the microsoft workspace using an API token

```
ghorg clone microsoft --scm=bitbucket --bitbucket-api-email=<your-atlassian-email> --token=<api-token>
```

1. Using environment variables (recommended for scripts)

```
export GHORG_BITBUCKET_API_TOKEN=<api-token>
export GHORG_BITBUCKET_API_EMAIL=<your-atlassian-email>
ghorg clone microsoft --scm=bitbucket
```

> Note: When using API tokens, ghorg automatically uses `x-bitbucket-api-token-auth` as the Git username for clone operations. The email is only used for API calls to list repositories.

### App Password Authentication (Legacy)

> Note: Bitbucket has deprecated App Passwords. Consider using API Tokens instead.

1. Clone the microsoft workspace using an app-password

```
ghorg clone microsoft --scm=bitbucket --bitbucket-username=<your-username> --token=<app-password>
```

### OAuth Token Authentication

1. Clone the microsoft workspace using oauth token

```
Expand Down
8 changes: 6 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var (
cloneType string
scmType string
bitbucketUsername string
bitbucketAPIEmail string
color string
baseURL string
concurrency string
Expand Down Expand Up @@ -332,6 +333,8 @@ func InitConfig() {
getOrSetDefaults("GHORG_BITBUCKET_USERNAME")
getOrSetDefaults("GHORG_BITBUCKET_APP_PASSWORD")
getOrSetDefaults("GHORG_BITBUCKET_OAUTH_TOKEN")
getOrSetDefaults("GHORG_BITBUCKET_API_TOKEN")
getOrSetDefaults("GHORG_BITBUCKET_API_EMAIL")
getOrSetDefaults("GHORG_SCM_BASE_URL")
getOrSetDefaults("GHORG_PRESERVE_DIRECTORY_STRUCTURE")
getOrSetDefaults("GHORG_OUTPUT_DIR")
Expand Down Expand Up @@ -374,8 +377,9 @@ func init() {
cloneCmd.Flags().StringVar(&protocol, "protocol", "", "GHORG_CLONE_PROTOCOL - Protocol to clone with, ssh or https, (default https)")
cloneCmd.Flags().StringVarP(&path, "path", "p", "", "GHORG_ABSOLUTE_PATH_TO_CLONE_TO - Absolute path to the home for ghorg clones. Must start with / (default $HOME/ghorg)")
cloneCmd.Flags().StringVarP(&branch, "branch", "b", "", "GHORG_BRANCH - Branch left checked out for each repo cloned (default master)")
cloneCmd.Flags().StringVarP(&token, "token", "t", "", "GHORG_GITHUB_TOKEN/GHORG_GITLAB_TOKEN/GHORG_GITEA_TOKEN/GHORG_BITBUCKET_APP_PASSWORD/GHORG_BITBUCKET_OAUTH_TOKEN/GHORG_SOURCEHUT_TOKEN - scm token to clone with")
cloneCmd.Flags().StringVarP(&bitbucketUsername, "bitbucket-username", "", "", "GHORG_BITBUCKET_USERNAME - Bitbucket only: username associated with the app password")
cloneCmd.Flags().StringVarP(&token, "token", "t", "", "GHORG_GITHUB_TOKEN/GHORG_GITLAB_TOKEN/GHORG_GITEA_TOKEN/GHORG_BITBUCKET_APP_PASSWORD/GHORG_BITBUCKET_API_TOKEN/GHORG_BITBUCKET_OAUTH_TOKEN/GHORG_SOURCEHUT_TOKEN - scm token to clone with")
cloneCmd.Flags().StringVarP(&bitbucketUsername, "bitbucket-username", "", "", "GHORG_BITBUCKET_USERNAME - Bitbucket only: username associated with the app password (legacy auth)")
cloneCmd.Flags().StringVarP(&bitbucketAPIEmail, "bitbucket-api-email", "", "", "GHORG_BITBUCKET_API_EMAIL - Bitbucket only: Atlassian account email for API token authentication")
cloneCmd.Flags().StringVarP(&scmType, "scm", "s", "", "GHORG_SCM_TYPE - Type of scm used, github, gitlab, gitea, bitbucket or sourcehut (default github)")
cloneCmd.Flags().StringVarP(&cloneType, "clone-type", "c", "", "GHORG_CLONE_TYPE - Clone target type, user or org (default org)")
cloneCmd.Flags().BoolVar(&skipArchived, "skip-archived", false, "GHORG_SKIP_ARCHIVED - Skips archived repos, github/gitlab/gitea only")
Expand Down
47 changes: 40 additions & 7 deletions configs/configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ var (
// ErrNoBitbucketAppPassword error message when no app password found
ErrNoBitbucketAppPassword = errors.New("Could not find a valid bitbucket app password. GHORG_BITBUCKET_APP_PASSWORD or (--token, -t) must be set to clone repos from bitbucket, see 'BitBucket Setup' in README.md")

// ErrNoBitbucketAPIToken error message when no API token found
ErrNoBitbucketAPIToken = errors.New("Could not find a valid bitbucket API token. GHORG_BITBUCKET_API_TOKEN or (--token, -t) must be set to clone repos from bitbucket, see 'BitBucket Setup' in README.md")

// ErrNoBitbucketAPIEmail error message when no email found for API token auth
ErrNoBitbucketAPIEmail = errors.New("When using GHORG_BITBUCKET_API_TOKEN, you must also set GHORG_BITBUCKET_API_EMAIL or GHORG_BITBUCKET_USERNAME (your Atlassian account email), see 'BitBucket Setup' in README.md")

// ErrIncorrectScmType indicates an unsupported scm type being used
ErrIncorrectScmType = errors.New("GHORG_SCM_TYPE or --scm must be one of " + strings.Join(scm.SupportedClients(), ", "))

Expand Down Expand Up @@ -282,8 +288,20 @@ func getOrSetGitLabToken() {
}

func getOrSetBitBucketToken() {
// Check if API token is set from file path
apiToken := os.Getenv("GHORG_BITBUCKET_API_TOKEN")
if apiToken != "" && IsFilePath(apiToken) {
os.Setenv("GHORG_BITBUCKET_API_TOKEN", GetTokenFromFile(apiToken))
}

// Check if app password is set from file path
appPassword := os.Getenv("GHORG_BITBUCKET_APP_PASSWORD")
if appPassword != "" && IsFilePath(appPassword) {
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", GetTokenFromFile(appPassword))
}

var token string
if isZero(os.Getenv("GHORG_BITBUCKET_APP_PASSWORD")) && isZero(os.Getenv("GHORG_BITBUCKET_OAUTH_TOKEN")) {
if isZero(os.Getenv("GHORG_BITBUCKET_APP_PASSWORD")) && isZero(os.Getenv("GHORG_BITBUCKET_OAUTH_TOKEN")) && isZero(os.Getenv("GHORG_BITBUCKET_API_TOKEN")) {
if runtime.GOOS == "windows" {
return
}
Expand All @@ -294,6 +312,9 @@ func getOrSetBitBucketToken() {

if !isZero(os.Getenv("GHORG_BITBUCKET_USERNAME")) {
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", token)
} else if !isZero(os.Getenv("GHORG_BITBUCKET_API_EMAIL")) {
// If API email is set, assume API token auth
os.Setenv("GHORG_BITBUCKET_API_TOKEN", token)
} else {
os.Setenv("GHORG_BITBUCKET_OAUTH_TOKEN", token)
}
Expand Down Expand Up @@ -359,15 +380,27 @@ func VerifyTokenSet() error {
}

if scmProvider == "bitbucket" {
if os.Getenv("GHORG_BITBUCKET_OAUTH_TOKEN") == "" {
// Check for OAuth token first (takes precedence)
if os.Getenv("GHORG_BITBUCKET_OAUTH_TOKEN") != "" {
return nil
}

if os.Getenv("GHORG_BITBUCKET_USERNAME") == "" {
return ErrNoBitbucketUsername
// Check for new API token authentication
if os.Getenv("GHORG_BITBUCKET_API_TOKEN") != "" {
// API token requires either API email or username for API calls
if os.Getenv("GHORG_BITBUCKET_API_EMAIL") == "" && os.Getenv("GHORG_BITBUCKET_USERNAME") == "" {
return ErrNoBitbucketAPIEmail
}
return nil
}

if os.Getenv("GHORG_BITBUCKET_APP_PASSWORD") == "" {
return ErrNoBitbucketAppPassword
}
// Fall back to legacy App Password authentication
if os.Getenv("GHORG_BITBUCKET_USERNAME") == "" {
return ErrNoBitbucketUsername
}

if os.Getenv("GHORG_BITBUCKET_APP_PASSWORD") == "" {
return ErrNoBitbucketAppPassword
}
}

Expand Down
62 changes: 62 additions & 0 deletions configs/configs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func TestVerifyTokenSet(t *testing.T) {
os.Setenv("GHORG_SCM_TYPE", "bitbucket")
os.Setenv("GHORG_BITBUCKET_USERNAME", "")
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", "12345678912345678901")
os.Setenv("GHORG_BITBUCKET_API_TOKEN", "")
os.Setenv("GHORG_BITBUCKET_API_EMAIL", "")
os.Setenv("GHORG_BITBUCKET_OAUTH_TOKEN", "")
err := configs.VerifyTokenSet()
if err != configs.ErrNoBitbucketUsername {
tt.Errorf("Expected ErrNoBitbucketUsername, got: %v", err)
Expand All @@ -47,12 +50,71 @@ func TestVerifyTokenSet(t *testing.T) {
os.Setenv("GHORG_SCM_TYPE", "bitbucket")
os.Setenv("GHORG_BITBUCKET_USERNAME", "bitbucketuser")
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", "")
os.Setenv("GHORG_BITBUCKET_API_TOKEN", "")
os.Setenv("GHORG_BITBUCKET_API_EMAIL", "")
os.Setenv("GHORG_BITBUCKET_OAUTH_TOKEN", "")
err := configs.VerifyTokenSet()
if err != configs.ErrNoBitbucketAppPassword {
tt.Errorf("Expected ErrNoBitbucketAppPassword, got: %v", err)
}

})

t.Run("When cloning bitbucket with API token but no email or username", func(tt *testing.T) {
os.Setenv("GHORG_SCM_TYPE", "bitbucket")
os.Setenv("GHORG_BITBUCKET_USERNAME", "")
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", "")
os.Setenv("GHORG_BITBUCKET_API_TOKEN", "test_api_token")
os.Setenv("GHORG_BITBUCKET_API_EMAIL", "")
os.Setenv("GHORG_BITBUCKET_OAUTH_TOKEN", "")
err := configs.VerifyTokenSet()
if err != configs.ErrNoBitbucketAPIEmail {
tt.Errorf("Expected ErrNoBitbucketAPIEmail, got: %v", err)
}

})

t.Run("When cloning bitbucket with API token and email", func(tt *testing.T) {
os.Setenv("GHORG_SCM_TYPE", "bitbucket")
os.Setenv("GHORG_BITBUCKET_USERNAME", "")
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", "")
os.Setenv("GHORG_BITBUCKET_API_TOKEN", "test_api_token")
os.Setenv("GHORG_BITBUCKET_API_EMAIL", "[email protected]")
os.Setenv("GHORG_BITBUCKET_OAUTH_TOKEN", "")
err := configs.VerifyTokenSet()
if err != nil {
tt.Errorf("Expected no error, got: %v", err)
}

})

t.Run("When cloning bitbucket with API token and username fallback", func(tt *testing.T) {
os.Setenv("GHORG_SCM_TYPE", "bitbucket")
os.Setenv("GHORG_BITBUCKET_USERNAME", "[email protected]")
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", "")
os.Setenv("GHORG_BITBUCKET_API_TOKEN", "test_api_token")
os.Setenv("GHORG_BITBUCKET_API_EMAIL", "")
os.Setenv("GHORG_BITBUCKET_OAUTH_TOKEN", "")
err := configs.VerifyTokenSet()
if err != nil {
tt.Errorf("Expected no error, got: %v", err)
}

})

t.Run("When cloning bitbucket with OAuth token", func(tt *testing.T) {
os.Setenv("GHORG_SCM_TYPE", "bitbucket")
os.Setenv("GHORG_BITBUCKET_USERNAME", "")
os.Setenv("GHORG_BITBUCKET_APP_PASSWORD", "")
os.Setenv("GHORG_BITBUCKET_API_TOKEN", "")
os.Setenv("GHORG_BITBUCKET_API_EMAIL", "")
os.Setenv("GHORG_BITBUCKET_OAUTH_TOKEN", "oauth_token")
err := configs.VerifyTokenSet()
if err != nil {
tt.Errorf("Expected no error, got: %v", err)
}

})
}

func TestVerifyConfigsSetCorrectly(t *testing.T) {
Expand Down
33 changes: 33 additions & 0 deletions examples/bitbucket.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,45 @@ To view all additional flags see the [sample-conf.yaml](https://github.com/gabri

## Bitbucket Cloud

### API Token Authentication (Recommended)

Bitbucket has deprecated App Passwords in favor of API Tokens. This is the recommended authentication method.

**Creating the API Token:**
1. Go to your [Atlassian account settings](https://id.atlassian.com/manage/api-tokens)
2. Create a new API token
3. **Important**: Grant **all read scopes** (Account: Read, Workspace membership: Read, Projects: Read, Repositories: Read) to ensure ghorg can list and clone repositories

**Using the API Token:**

1. Clone the microsoft workspace using an API token

```
ghorg clone microsoft --scm=bitbucket --bitbucket-api-email=<your-atlassian-email> --token=<api-token>
```

1. Using environment variables (recommended for scripts)

```
export GHORG_BITBUCKET_API_TOKEN=<api-token>
export GHORG_BITBUCKET_API_EMAIL=<your-atlassian-email>
ghorg clone microsoft --scm=bitbucket
```

> Note: When using API tokens, ghorg automatically uses `x-bitbucket-api-token-auth` as the Git username for clone operations. The email is only used for API calls to list repositories.

### App Password Authentication (Legacy)

> Note: Bitbucket has deprecated App Passwords. Consider using API Tokens instead.

1. Clone the microsoft workspace using an app-password

```
ghorg clone microsoft --scm=bitbucket --bitbucket-username=<your-username> --token=<app-password>
```

### OAuth Token Authentication

1. Clone the microsoft workspace using oauth token

```
Expand Down
Loading
Loading