Developing#
pw_ghish: GitHub CLI-like interface for Gerrit code reviews and CI checks
This section is for contributors developing, testing, and extending pw_ghish
(//pw_ghish:gh-ish).
Building and running locally#
pw_ghish is written in Go and built with Bazel:
$ bazelisk build //pw_ghish:gh-ish
When you run ./gh from the repository root, the wrapper script checks
out/gh/gh-ish-<commit> and automatically rebuilds the binary when HEAD
changes. During local development on uncommitted changes, you can either run
bazelisk run //pw_ghish:gh-ish -- <args> or delete cached binaries in
out/gh/ to force a fresh build.
Running tests#
pw_ghish uses two test suites:
Hermetic unit tests (run in presubmit):
$ bazelisk test //pw_ghish/...
Unit tests run under a package
TestMain(hermetic_main_test.go) that makes credential tool lookup and token subprocesses fail and refuses non-loopback HTTP; install credentials withWithHermeticAuth.Live integration tests (run manually with workstation credentials against
pigweed-review.googlesource.com, Buildbucket, and Buganizer):$ go test -v -tags=live ./pw_ghish -run TestLive
See Agent evaluation for details on the live test suite and agent evaluation scenarios.
Engineering invariants#
Changes to pw_ghish follow strict CLI reliability and testing conventions
(see pw_ghish/AGENTS.md for full details):
Error propagation (
RunE): Every Cobra command usesRunEand returns non-zero exit codes on RPC, subprocess, or validation failures.os.Exitis called only inpw_ghish/cmd/main.go.Output stream discipline: Write primary command output to
cmd.OutOrStdout()and diagnostics/warnings tocmd.ErrOrStderr()so unit tests can capture both streams hermetically.Broken pipes:
gh ... | head -1must never leave a mutation half done.pw_ghish.MainhandlesSIGPIPE(so writes returnEPIPEinstead of killing the process) and wraps piped stdout/stderr in aPipeGuard: once the reader is gone,mutationandmetacommands keep running with output discarded and exit141(128+SIGPIPE) after finishing, whileview,list, andstreamcommands stop at the first failed write, likegit log | head. Real failures keep their own exit code. With2>&1both streams share one guard, so child output stays in order. Subprocesses that receive these writers run viarunBoundingOrphanedPipes, which setsWaitDelayso a detached grandchild holding the pipe cannot hang the command. The./ghwrapper likewise keeps Bazel build chatter off stdout and, unless stderr is a terminal, captures it and replays it only if the build fails, so a closed pipe cannot abort the wrapper before it runs the binary.Commit trailer preservation: Commands that modify commit messages (such as
pr create,pr push, andissue create --amend) parse and preserve existing Git trailers (Change-Id:,Bug:,Fixed:,Reviewed-on:).Actionable error messages: Errors state what failed, why (the precondition or server state), the exact command to fix it, and an inspection command to check current state.
UI design language and context budgeting#
pw_ghish serves both interactive terminal users and coding agents operating
under strict context-window budgets. New and modified commands should use the
shared formatting helpers in pw_ghish/ui.go and pw_ghish/environment.go.
Answer-first layout and progressive disclosure#
When writing or updating command output:
Surface verdicts and next steps: Include a
Verdict:summary line and an actionableNext:command hint so callers see the current state and recommended follow-up action immediately.Order actionable items first: Render failing or running checks and unresolved review threads before passing or resolved items, and group repeated failure diagnostics via
ClusterFailedBuilds.Fold high-volume details behind
--allor--path: Collapse passing check rows, resolved comment threads, automated bot dumps, and large or generated diffs by default. Whenever output is folded, print a 1-line summary showing the exact--allor--path <glob>command to expand it.Keep tables compact: Target
<100columns without decorative===separator walls.
UI primitives, glyphs, and receipts#
Shared terminal formatting primitives live in pw_ghish/ui.go and
pw_ghish/environment.go:
Primitive |
Usage and rules |
|---|---|
Canonical BMP glyphs |
Use single-width BMP constants from |
TTY, pipe, and agent color discipline |
Never embed raw ANSI escape literals outside |
Width-safe column padding |
Pad table cells using |
Structured mutation receipts |
Every mutating command (annotated |
New command checklist#
Follow this checklist when adding or updating a runnable Cobra command:
Declare
ghish:kindinline in thecobra.Commandliteral:Annotations: map[string]string{KindAnnotation: KindView}(usepw_ghish.KindAnnotationfrompw_ghish/worktree). Pick one kind frompw_ghish/command_kind.go:mutation: Changes Gerrit, Buganizer, LUCI, git, or local state. Commands that both mutate and print are mutations; give a read-only mode its own command rather than an alias.view: Renders one object in detail.list: Renders a collection.stream: Long-running, streaming, or passthrough output.meta: Plumbing invoked by tools rather than people.
When a flag changes the behavior (for example
pr checks --watch), declare the default kind.mutation: End with a
PrintReceiptorFormatReceiptreceipt that carries aNext:hint on its success path.view and list: Include a
Verdict:line and aNext:hint, and fold output past its budget behind--allor--pathwith a 1-line unfold hint. If stdout is a raw artifact meant for other tools (such aspr diff), declareOutputAnnotation: OutputRawinstead.Register in the kind smoke harness: Add one
kindSmokeArgsentry inpw_ghish/kind_contract_test.gokeyed by the command path (for example"gh-ish pr view": {"pr", "view", "12345"}), usingkindSmokeSetupfor any unmeasured preparation steps (see Kind smoke harness).Stay within subprocess and RPC budgets: Construct all external processes via
ExecCommand/ExecCommandContextinpw_ghish/procexec.goand stay within the default count budgets for the command’s kind. If extra work is inherent, pin upper bounds inkindSmokeBudgetPins(or exact counts inperfBudgetPinsinpw_ghish/perfbudget_test.go) or setghish:budget-exempt(BudgetExemptAnnotation) with a reason, and justify the budget in the CL description.Classify flags and update docs: Add a
//ghish:flagdirective above every new flag registration (see Compat policy), document new flags and subcommands in the correspondingpw_ghish/*.rstreference page, and ensure--json <fields>flags reject unknown fields withUnknownJSONFieldsError(Rendererhandles this automatically; see JSON output contract).
Testing UI, JSON, and performance rules#
Kind smoke harness#
Adding a kindSmokeArgs entry in pw_ghish/kind_contract_test.go
automatically tests your command in a hermetic fixture
(pw_ghish/kind_fixture_test.go) providing a temporary git repository on
change 12345, a mock Buganizer issue 345678, mock Gerrit/Buildbucket/Issue
Tracker servers, and an isolated wt pool with PATH restricted to git
and non-loopback HTTP blocked.
Each command is checked against its kind’s contract and default budgets:
Kind |
Output contract |
Default budget (bytes / subprocesses / RPCs) |
|---|---|---|
|
A |
1024 / 4 / 8 |
|
A |
2048 / 2 / 6 |
|
A |
2048 / 2 / 8 |
Every command is also checked for decorative noise and duplicate identical HTTP
requests or subprocesses. Do not add entries to kindSmokeContractGaps,
kindSmokeDuplicateSubprocesses, or kindSmokeSkip for new code; fix the
command instead.
Golden snapshots#
When adding or changing major user-facing output layouts, add or update a case
in pw_ghish/golden_test.go (snapshots live under
pw_ghish/testdata/golden/<case>.expected). Keep fixtures deterministic by
using fixed timestamps and stable ordering.
When you intentionally change command output, regenerate the snapshots and
review the resulting .expected diff alongside your code change:
$ bazelisk run //pw_ghish:pw_ghish_test -- -test.run TestGolden -update
Delete stale .expected files manually if you remove or rename a test case.
JSON output contract#
Every command with a --json flag is automatically tested via its
kindSmokeArgs entry in pw_ghish/json_contract_test.go. When adding or
modifying --json output, follow three rules:
Single JSON value on stdout: Write only the JSON payload to
cmd.OutOrStdout(). Send progress, warnings, or receipts tocmd.ErrOrStderr()(or omit receipts in JSON mode).Bare ASCII enum values: Enum-like fields (such as
status,state,bucket,kind,residency,mode,priority, or keys ending in_status,_state,Status,State, orBadge) must contain bare ASCII identifiers ("LOCAL_WIP"), never terminal glyphs ("● LOCAL_WIP"). Attach glyphs only in text rendering (for example viaStatusBadge.Display).Non-null empty collections: Initialize empty slices and maps so they marshal as
[]and{}, nevernull.
Performance budgets#
Performance tests in pw_ghish/perfbudget_test.go and
pw_ghish/worktree/perfbudget_test.go pin deterministic counts (constructed
subprocesses via ExecCommand and HTTP requests), never wall-clock time.
When a budget test fails, the failure output lists every recorded subprocess or HTTP request:
Prefer removing the extra work by memoizing on
Config/GitClient, batching Gerrit queries, or reusing prefetched state.If a count change is intentional (including a decrease that triggers the stale-budget check), update the pin in
perfbudget_test.goorkindSmokeBudgetPinsin the same CL and explain why in the CL description.
To run informational wall-clock benchmarks locally against production Gerrit:
$ go test -v -tags=live ./pw_ghish -run TestLiveBench
Set GH_ISH_BIN to benchmark a prebuilt binary and
GH_ISH_PERF_UPDATE_BASELINE=1 to update
pw_ghish/testdata/perf_baseline.json.
Contributor sections#
Compat policy: Rules for aligning flags with upstream GitHub CLI, unbound single-letter shorthands, and the checklist for adding new flags.
Agent evaluation: Live integration test runbook (
live_test.go) and coding agent behavioral evaluation scenarios.Status & roadmap: Current subcommand status and planned work for multi-project configuration.
Source code: Browse the
pw_ghishGo implementation in Pigweed Code Search.Open issues: View open
pw_ghishissues in Buganizer.