Skip to content

feat: add env-var-driven headless authentication (BKT_HOST + BKT_TOKEN)#138

Merged
avivsinai merged 7 commits intoavivsinai:masterfrom
ekrako:134-bkt-token-headless-auth
Apr 10, 2026
Merged

feat: add env-var-driven headless authentication (BKT_HOST + BKT_TOKEN)#138
avivsinai merged 7 commits intoavivsinai:masterfrom
ekrako:134-bkt-token-headless-auth

Conversation

@ekrako
Copy link
Copy Markdown
Contributor

@ekrako ekrako commented Apr 8, 2026

Summary

Adds support for fully config-free CLI usage via environment variables.
Closes #134. When `BKT_TOKEN` and `BKT_HOST` are set, every command works in
containers and CI pipelines without a prior `bkt auth login` or
`bkt context create` step.

Changes

`internal/secret/store.go`

  • Exports six new `BKT_*` constants (`EnvHost`, `EnvUsername`, `EnvAuthMethod`,
    `EnvProject`, `EnvWorkspace`, `EnvRepo`) alongside the existing `EnvToken`

`pkg/cmdutil/context.go`

  • `hostFromEnv(rawURL)` — builds an ephemeral `*config.Host` from env vars;
    canonicalises `bitbucket.org` / `api.bitbucket.org` → `https://api.bitbucket.org/2.0\`
    using exact hostname matching (prevents false positives like `bitbucket.org.corp.com`);
    defaults DC PAT-only flows to bearer auth when `BKT_USERNAME` is absent;
    requires `BKT_USERNAME` for Cloud (always basic auth); errors early if
    `BKT_AUTH_METHOD=basic` is set without `BKT_USERNAME`
  • `contextFromEnv()` — builds an ephemeral `*config.Context` from `BKT_PROJECT` /
    `BKT_WORKSPACE` / `BKT_REPO`; `applyRemoteDefaults` runs so git-remote inference
    works from cloned CI repos even without those vars
  • `ResolveContext` — env fallback when no active context; prefers the saved host
    entry when `BKT_HOST` matches an existing config key (preserves persisted
    `username` / `auth_method`); overlays `BKT_USERNAME` / `BKT_AUTH_METHOD` if set
    so headless runs are not silently bound to stale stored credentials
  • `ResolveHost` — env fallback in `case 0` (no hosts), `case 1` (single host,
    `BKT_HOST` may target a different server), and `default` (multiple hosts,
    `BKT_HOST` disambiguates); same env auth-field overlay applied to all saved-host paths;
    `--host X` flag also synthesises an ephemeral host when X is not in config
  • `errTokenSetButNoHost` — actionable error when `BKT_TOKEN` is set without
    `BKT_HOST`; errors from `hostFromEnv` are propagated rather than swallowed

`pkg/cmdutil/context_test.go`

  • 29 new unit tests covering helpers, all three `ResolveHost` switch paths, and
    `ResolveContext` env fallback; `t.Setenv` throughout so no env leakage between tests

`README.md` / `skills/bkt/SKILL.md`

  • New "Environment Variables" reference table documenting all `BKT_*` vars
  • Headless usage examples for both Data Center and Bitbucket Cloud
  • Updated Bitbucket Pipelines section to show the config-free flow

Testing

  • `go test ./...` — 882 tests pass (29 net new)
  • `go vet ./...` — clean
  • `pre-commit run` — all hooks pass
  • Key scenarios covered by tests:
    • DC and Cloud host synthesis from env vars
    • `bitbucket.org` / `api.bitbucket.org` (bare, `/2.0`, and canonical forms)
      all canonicalise to `https://api.bitbucket.org/2.0\`
    • `bitbucket.org.corp.com` style DC host NOT misclassified as Cloud
    • DC without username defaults to bearer auth; explicit basic+no-username errors early
    • Cloud without `BKT_USERNAME` errors early with an actionable message
    • `BKT_USERNAME` / `BKT_AUTH_METHOD` env vars override stale saved host fields
    • `BKT_HOST` disambiguates single-host and multi-host configs
    • Saved host fields (`username`, `auth_method`) preserved when no env override
    • `applyRemoteDefaults` runs for env-backed contexts (git-remote inference)
    • Fallback NOT triggered when `BKT_TOKEN` is absent

New environment variables — no CI configuration needed; these are opt-in,
read at runtime. Existing users with a config file are unaffected.

Risks

  • None. All changes are additive — every existing error path is preserved when
    `BKT_TOKEN` / `BKT_HOST` are unset. No new dependencies, no CI/CD changes,
    no breaking API surface.

@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 8, 2026

Codecov Report

❌ Patch coverage is 75.23810% with 26 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/cmdutil/context.go 75.23% 13 Missing and 13 partials ⚠️

📢 Thoughts on this report? Let us know!

@avivsinai avivsinai marked this pull request as ready for review April 9, 2026 06:53
Copy link
Copy Markdown
Owner

@avivsinai avivsinai left a comment

Choose a reason for hiding this comment

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

Thanks again Eran — this is excellent work. The env-var-driven headless auth is exactly the right design, and with OAuth Phase 1 queued up too, you're really pushing bkt forward. We're closing our tactical #141 in favor of this.

Must fix

1. Copy saved host before env overlay
The env overlay in ResolveContext and ResolveHost mutates cfg.Hosts[envKey] in place. If any future code path calls cfg.Save() after resolution, env-sourced Username/AuthMethod values would be silently persisted to the user's config file. Fix: shallow-copy before mutating:

clone := *saved
saved = &clone

Three sites: ResolveContext, ResolveHost case 1, ResolveHost default.

(Note: MarshalYAML already strips Token, which is great defense-in-depth — but Username and AuthMethod aren't stripped, so the copy is needed.)

2. Fix BKT_AUTH_METHOD docs
README and SKILL.md say default is basic, but the actual code behavior is: DC defaults to bearer when BKT_USERNAME is absent, and Cloud always forces basic. No scenario where the default is unconditionally basic. Update docs to reflect actual behavior.

Suggestions (non-blocking)

  • DRY up env-overlay logic: The check-saved-host / loadHostToken / overlay-username-authmethod pattern is duplicated 4x across ResolveContext and the three ResolveHost switch branches. Consider extracting a resolveEnvHost() helper (~40 lines saved, consistency by construction).
  • Hint when BKT_HOST is set without BKT_TOKEN: Currently silently ignored with a generic "no hosts configured" error. A "BKT_HOST is set but BKT_TOKEN is not — did you forget to set BKT_TOKEN?" hint would help CI debugging.
  • Table-driven tests: The 14 TestHostFromEnv* functions could be one table-driven TestHostFromEnv with subtests, per project convention.

What's done well

  • Cloud detection via exact hostname matching is correct and the bitbucket.org.corp.com test case proves it's not just theoretical
  • errTokenSetButNoHost is actionable and tells users exactly what to do
  • The env fallback is truly additive — every existing path is preserved when env vars are unset
  • Test coverage is thorough: 29 tests covering all ResolveHost switch arms, ResolveContext env path, and edge cases

ekrako added 2 commits April 9, 2026 11:20
…T_TOKEN)

Fixes avivsinai#134. When BKT_TOKEN and BKT_HOST are set, all commands work in
containers and CI pipelines without a prior `bkt auth login` or
`bkt context create` step.

- Add hostFromEnv / contextFromEnv helpers that synthesise ephemeral
  host+context from env vars; never written to disk
- Patch ResolveContext and ResolveHost (case 0, case 1, default, and
  --host override paths) to fall back to env vars when no config match
- Prefer saved host entry when BKT_HOST key matches config, preserving
  persisted username / auth_method / auth scheme
- Canonicalise bitbucket.org and api.bitbucket.org to the API origin
  using exact hostname matching (avoids bitbucket.org.corp.com false
  positives); bare api.bitbucket.org also normalised to /2.0 base
- applyRemoteDefaults runs for env-backed contexts so git-remote
  inference fills project/workspace/repo in cloned CI workspaces
- Export BKT_HOST, BKT_USERNAME, BKT_AUTH_METHOD, BKT_PROJECT,
  BKT_WORKSPACE, BKT_REPO constants from internal/secret
- 24 new unit tests; all existing 853 tests continue to pass
- Update README.md and skills/bkt/SKILL.md with env-var reference
  table and headless usage examples for DC and Cloud
P1: hostFromEnv now defaults DC PAT-only flows to bearer auth when neither
BKT_USERNAME nor BKT_AUTH_METHOD is set, so headless DC usage works
out-of-the-box without BKT_AUTH_METHOD=bearer. Explicitly requesting
basic auth without a username now returns a clear error. Cloud now errors
immediately when BKT_USERNAME is absent since basic auth is always required.

P2: When BKT_HOST matches a saved config host, BKT_USERNAME and
BKT_AUTH_METHOD are now overlaid onto the saved entry so headless runs
are not silently bound to stale stored credentials.
@ekrako ekrako force-pushed the 134-bkt-token-headless-auth branch from 8772619 to 05a846a Compare April 9, 2026 08:21
Copy link
Copy Markdown
Owner

@avivsinai avivsinai left a comment

Choose a reason for hiding this comment

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

Thanks for the new commits Eran — the docs reorg into rules/headless.md is a nice improvement and the auth-method-rules table there is correct.

Both original must-fix items are still open though:

1. Shallow-copy saved host before env overlay (still blocking)

The overlay still mutates cfg.Hosts[envKey] in place (saved.Username = u / saved.AuthMethod = m) at three sites. If anything downstream calls cfg.Save(), env-only overrides get silently persisted to disk.

Rather than sprinkling clone := *saved; saved = &clone at each site (manual discipline that breaks the moment someone adds a 4th caller), extract a helper that takes the saved host by value:

func resolveEnvHost(base config.Host) *config.Host {
    // overlay env fields onto the copy — caller's map entry is untouched
}

The type system enforces immutability — you can't mutate the original because you never have a pointer to it in the overlay function. This also collapses the duplicated overlay logic in ResolveContext and both ResolveHost branches into one call site.

2. README BKT_AUTH_METHOD default (still blocking)

README.md line 68 still says basic (default). The new rules/headless.md table is correct (DC defaults to bearer when no username), but the README needs to match.

ekrako added 2 commits April 10, 2026 15:47
…EADME auth method default

- Extract mergeEnvHost(executable, key string, base config.Host) helper
  that takes the saved host entry by value, so env-sourced Username/
  AuthMethod overlays never mutate cfg.Hosts in place. Replaces the
  three inline overlay blocks in ResolveContext and both ResolveHost
  switch branches with a single call site.
- README: BKT_AUTH_METHOD description now reflects actual behaviour —
  DC defaults to bearer when BKT_USERNAME is absent; Cloud always basic.
… BKT_TOKEN

- Collapse 13 individual TestHostFromEnv* functions into one
  table-driven TestHostFromEnv with subtests, per project convention.
- Add errHostSetButNoToken sentinel; return it from ResolveContext and
  ResolveHost (case 0 and default) when BKT_HOST is set but BKT_TOKEN
  is absent, giving a "did you forget to set BKT_TOKEN?" hint instead
  of the generic no-hosts/no-context error.
- Update two existing tests whose BKT_HOST fixture now triggers the hint;
  add TestResolveHostHostSetButNoToken and TestResolveContextHostSetButNoToken.
@ekrako
Copy link
Copy Markdown
Contributor Author

ekrako commented Apr 10, 2026

Must fix #1 — shallow-copy / mergeEnvHost

Done. Extracted mergeEnvHost(executable, key string, base config.Host) *config.Host — the saved host is passed by value so the type system prevents mutation of cfg.Hosts at all three call sites. Collapses the duplicated overlay logic as a side effect. Commit 9bda315.

Must fix #2 — README BKT_AUTH_METHOD default

Fixed in the same commit. BKT_AUTH_METHOD now reads: "DC defaults to bearer when BKT_USERNAME is absent; Cloud always uses basic."

Non-blocking suggestions

All three addressed:

  • DRY env-overlay: covered by the mergeEnvHost helper above.
  • Hint when BKT_HOST set without BKT_TOKEN: added errHostSetButNoToken sentinel, surfaced in ResolveContext and ResolveHost (case 0 + default). Commit a0d20f1.
  • Table-driven TestHostFromEnv*: 13 individual functions collapsed into one TestHostFromEnv with subtests. Same commit.

Copy link
Copy Markdown
Owner

@avivsinai avivsinai left a comment

Choose a reason for hiding this comment

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

Thanks Eran — all looks great. CI green on 0d10f10. Approving.

@avivsinai avivsinai merged commit 1942136 into avivsinai:master Apr 10, 2026
7 checks passed
@barryvdh
Copy link
Copy Markdown

I'm not sure if I'm doing this correctly, but running headless like this:

        script:
          - export BKT_VERSION="0.25.0"
          - export BKT_HOST="https://bitbucket.org"
          - curl -sL "https://github.com/avivsinai/bitbucket-cli/releases/download/v${BKT_VERSION}/bkt_${BKT_VERSION}_linux_x86_64.tar.gz" | tar xz -C /tmp && install /tmp/bkt /usr/local/bin/

And I have BKT_TOKEN set in my environment. It works, but it keeps saying

Error: BKT_HOST: BKT_USERNAME is required for Bitbucket Cloud; set it to your Atlassian account email
Error: BKT_HOST: BKT_USERNAME is required for Bitbucket Cloud; set it to your Atlassian account email

The documentation says I need it, but why would I need that? If I look at https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-commit-commit-pullrequests-get I'm not sure why I would need to use a username, I don't see it in the documentation.

@barryvdh
Copy link
Copy Markdown

Or maybe to explain, it doesn't need it for ALL requests. I was tring to use it to view the diff and create comments, which can use Repository tokens, which I guess are different?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BKT_TOKEN requires regular authentication first

3 participants