Skip to content

Latest commit

 

History

History
240 lines (202 loc) · 11.9 KB

File metadata and controls

240 lines (202 loc) · 11.9 KB

CLAUDE.md

REQUIRED READING — every session, before any change

All sessions must follow the Bitrise CLI patterns guide — the team's internal research doc on CLI conventions, cross-referenced against gh, glab, bk, gcloud, aws, heroku, clig.dev, and the Heroku CLI Style Guide. (Bitrise maintainers: it lives in the team Confluence space.)

That doc is the source of truth for CLI conventions: command structure, noun/verb naming, output formats, flag conventions, config precedence, help format, stdout/stderr discipline, exit codes. Re-read it before changing any user-facing surface (flags, command names, error messages, help text) or adding new commands. If a proposed change conflicts with the guide, raise the conflict — don't go around it. The locked-in conventions below capture the guide's decisions that this codebase depends on; when the guide isn't handy, mirror what gh does — it's the closest-spirit reference CLI for our use case.

What this is

bitrise-cli is a CLI for Bitrise platform resources (builds, apps, workflows). cmd/ handlers call into service types in internal/build, internal/app, etc., which in turn call the real Bitrise API through the HTTP client in bitriseapi/. The layering (below) is strict: keep HTTP and business logic out of cmd/.

The canonical binary name is bitrise-cli. br is documented as an optional shell alias / symlink, NOT shipped as the binary name. The patterns guide flags a real collision with [broot]'s br shell function; do not rename the binary to br without a team decision.

Layering — keep it intact

cmd/                 cobra presentation only: flag parsing, output formatting,
                     calling into services. NO business logic, NO HTTP.
internal/build       service layer for build operations
internal/app         service layer for app + workflow operations
internal/auth        Auth file (auth.yaml): the access token only
internal/config      Config + Path/Load/Save, LoadDir, Resolve, ctx helpers
internal/output      Format + generic Render; Human and JSON formats
bitriseapi/          HTTP client; called by the internal services.

cmd handlers do exactly: parse flags → call service method → render result. The services hold a *bitriseapi.Client and own all HTTP/business logic, so new commands extend the services without touching the cmd layer's shape.

Locked-in conventions (per the patterns guide)

  • Output format flag: --output human|json, -o. human is the default. JSON output is a stable contract — additive changes only; no breaking renames without a major version bump.
  • stdout vs stderr: stdout carries the answer (data, JSON, table rows). stderr carries diagnostics, confirmations, progress. JSON mode never mixes diagnostics into stdout. Errors via cobra's RunE go to stderr.
  • Output scheme: colors, symbols ( / / ), table layout, key/value patterns, ErrWriter usage, and JSON contracts are documented in docs/output-scheme.md. Follow it when adding or changing any human-readable output.
  • Config precedence (highest to lowest):
    1. CLI flag (--output folded in by persistentPreRun; per-command flags like --app are layered in the command handler itself)
    2. Environment variables (BITRISE_TOKEN, BITRISE_APP_ID, BITRISE_WORKSPACE_ID, BITRISE_OUTPUT, BITRISE_API_BASE_URL)
    3. Per-directory file: .bitrise-cli.yml in CWD or any ancestor
    4. Global file: $XDG_CONFIG_HOME/bitrise/config.yaml (falls back to ~/.config/bitrise/config.yaml)
    5. Auth file (token + type only): $XDG_CONFIG_HOME/bitrise/auth.yaml
    6. Built-in defaults
  • Token storage: bitrise-cli auth login writes the token to auth.yaml. In an interactive terminal it defaults to browser OAuth, storing a managed PAT that auto-refreshes (--with-token pastes/pipes a token, --email mints one via email/password). PAT and WAT tokens are stored identically (same wire format — no type field). Resolve order for tokens: env > auth.yaml.
  • File perms: both config.yaml and auth.yaml are 0600; parent dir 0700.
  • Bitrise verbs: build trigger (not create); build abort (not cancel) when added; build rerun for re-runs; view is the detail verb.
  • Singular nouns: app, build, workflow — never plural.
  • Identifier term: ID, never slug (user-facing). Every user-visible surface — help text, flag/arg metavars (APP_ID, BUILD_ID), output labels (ID:) and table headers (ID), config keys (app_id, default_workspace_id), env vars (BITRISE_APP_ID), and --output json field names (id, app_id, build_id, workspace_id) — says ID. The Bitrise API calls these slug on the wire; that term lives only in the bitriseapi/ layer and in internal Go identifiers (e.g. AppSlug, OrgSlug) — it must never reach the user.
  • Workspace, never organization/org/owner (user-facing). The entity that owns an app is a workspace everywhere a user sees it: the --workspace flag, the WORKSPACE column, the Workspace: label. "Organization"/"org" is obsolete. An app's owner is always a workspace (user-owned apps are unsupported), so apps use "workspace", not "owner". (rde template's Owner column is the template creator's email — a different concept — and keeps "Owner".)
  • PROJECT_TYPE, not PROJECT: the platform column/label (ios, android, …) is "Project type" (PROJECT_TYPE as an ALL-CAPS table header, Project type: as a key/value label). This reflects the API's project_type field — keep it in internal code but don't use "project" as a standalone user-facing term for an app.
  • Stdin via -: bitrise-cli config set token - reads from stdin so secrets stay out of shell history. Apply this pattern to any new secret-accepting command.
  • -q/--quiet suppresses non-error stderr ("Saved output", etc.). Errors and primary stdout output ignore it.
  • Update checks: best-effort "new release available" notice on stderr (never stdout/JSON), at most once per 24h against GitHub's public Releases API — nothing is sent to Bitrise. Gated to released builds on an interactive TTY; skipped in CI, under -q, in --output json, and via BITRISE_CLI_NO_UPDATE_NOTIFIER. Mechanics in internal/update; the when-to-show policy is shouldCheckForUpdate in cmd/. Full spec: docs/output-scheme.md §11.

Deferred — don't add without a ticket

These are listed in the patterns guide as standard features but are intentionally out of scope right now. Don't reopen the discussion as part of an unrelated change:

  • bitrise-cli api raw HTTP wrapper
  • --json fields projection + --jq expression
  • --dry-run for mutating commands
  • Workspace concept (workspace use, --workspace)
  • Confirmation prompts on destructive ops (no destructive ops exist yet)
  • OS-keychain token storage (currently in auth.yaml 0600)
  • PAT vs WAT type tagging — they have identical wire format, so we store them as one opaque token. Add a type field back if/when cross-workspace operations gain WAT-aware warnings.
  • bitrise.yml-based context auto-detection
  • Telemetry, plugin system, init wizard
  • Per-directory config writing via bitrise-cli config set (currently set/unset only modify the global file; per-dir is hand-edited)

Build, vet, run

Prefer the make targets — they're the source of truth and also what CI runs:

  • make build — binary lands at the repo root, gitignored
  • make tidygo mod tidy + ensures go.mod/go.sum are unchanged
  • make fmt — formatting check (must produce no output)
  • make vet — static analysis
  • make lint — runs golangci-lint via go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@<pinned-version>. Version is pinned in the Makefile; no separate install step needed. The compiled binary is cached in GOCACHE, so subsequent runs are fast.
  • make lint-fix — same as make lint but applies auto-fixes (--fix).
  • make testgo test -race -count=1 -timeout=5m ./...
  • make docs — regenerate docs/cli/ (markdown reference, one file per command, rendered from the cobra command tree via tools/gendocs). Run this whenever a command, flag, or help text changes — CI runs make docs-check and fails if the committed files drift.
  • Run the full quality gate via bitrise run test
  • When adding tests, put them in the same package as the file under test
  • go.mod is at module path github.com/bitrise-io/bitrise-cli

Lint compliance — required before declaring any task done

Always run make lint fix and fix all issues before reporting work as complete. go vet alone is not sufficient.

errcheck — the most common failure

Every fmt.Fprintf / fmt.Fprintln / fmt.Fprint return value must be captured. Two patterns cover all cases:

Single write with early return — capture inline:

_, err := fmt.Fprintln(w, "message")
return err

Multiple sequential writes — use cmdutil.NewErrWriter, check once at the end:

ew := cmdutil.NewErrWriter(w)   // or NewErrWriter(tabwriter)
ew.Ln("header")
ew.F("row %s\n", value)
return ew.Err

Never write fmt.Fprintln(w, x) or fmt.Fprintf(w, ...) without capturing the return. The linter will reject it every time.

Versioning hooks

cmd.version, cmd.commit, and cmd.buildNumber are package-level vars so CI can inject real values via -ldflags:

go build -ldflags "-s -w \
                  -X github.com/bitrise-io/bitrise-cli/cmd.version=X.Y.Z \
                  -X github.com/bitrise-io/bitrise-cli/cmd.commit=$GIT_SHA \
                  -X github.com/bitrise-io/bitrise-cli/cmd.buildNumber=$BUILD_NO"

buildNumber is the CI build number (from $BITRISE_BUILD_NUMBER, injected by the release pipeline) so a published binary can be traced back to the build that produced it. It's empty for dev builds and omitted from version output when empty.

When ldflags aren't set, runtime/debug.ReadBuildInfo() fills in vcs.revision and vcs.time so bitrise-cli version still has commit info.

Releasing

Releases are tag-triggered: push a semver tag (vX.Y.Z) and the release workflow in bitrise.yml runs GoReleaser, which cross-compiles every supported platform and publishes a draft GitHub release (archives + checksums.txt). A human reviews the generated notes and clicks publish.

  • .goreleaser.yaml is the single source of truth for release builds (platform matrix, ldflags). Keep its ldflags in sync with the Makefile's dev-build LDFLAGS.
  • make release-check validates the config; make release-snapshot builds all platforms into dist/ without tagging or publishing. The snapshot workflow in bitrise.yml does the same on CI for ad-hoc binaries.
  • GoReleaser is version-pinned in the Makefile and run via go run, like golangci-lint.
  • The CI release needs a GITHUB_TOKEN secret with contents:write (configured on the Bitrise app, not in the repo).

Known nits

  • Cobra auto-binds -v to --version. The patterns guide reserves -v for --verbose. Reclaim it when adding --verbose.
  • A malformed config file (global or per-dir) makes every command fail, including bitrise-cli config list/set/unset. Recovery is hand-editing or deleting the file. A config reset escape hatch is a reasonable follow-up.

README command list

The command overview in README.md (between the commands-overview markers) and the per-command pages in docs/cli/ are generated by tools/gendocs — never edit them by hand. After any change to a command, flag, or help text, run make docs and commit the result; CI runs make docs-check and fails on drift.

When in doubt

Follow the locked-in conventions above and the patterns guide they come from. If neither covers the case, mirror what gh does — that's the closest-spirit reference CLI for our use case.