GitHub Actions Watcher
I kept alt-tabbing to GitHub to check if my CI was done. Got tired of it, so I built a terminal UI that shows workflow runs inline.
brew install dzoba/tap/ghaw
ghaw
It auto-detects the repo from your git remote, pulls recent runs via the gh CLI, and polls for updates. Arrow keys to navigate, Enter to drill into a run and see individual jobs and steps with timing info.
v1: Ink (TypeScript)
The first version was built with Ink -- React, but for the terminal. Components render to the TTY instead of the DOM. It sounds weird but it works well for interactive CLIs.
Data comes from the GitHub CLI (gh run list, gh run view), which handles auth and pagination. No API tokens to configure -- if gh works, this works.
I wanted a countdown timer in the footer showing seconds until the next poll. Should be simple -- decrement a number every second. But Ink redraws the entire terminal on any state change. A per-second timer meant the full screen flickered every second. I tried suppressing unnecessary rerenders, batching state updates, all of it. The flicker was baked into Ink's rendering model.
v2: Go/Bubbletea
The fix was switching to Bubbletea, the framework the gh CLI itself uses. Bubbletea does differential rendering -- only changed lines get redrawn. A countdown timer ticking every second updates one line in the footer. No flicker.
The rewrite was a full replacement: all TypeScript source removed, npm distribution replaced with GitHub Releases and Homebrew. The binary is self-contained -- no Node runtime required.
Two independent tick chains drive the UI: a poll tick (every N seconds, triggers a fetch) and a countdown tick (every 1 second, decrements the counter). Change detection compares JSON-serialized state before updating, so the screen only rerenders when data actually changes.
v2.0: Multi-repo tabs
The single-repo limitation got old fast. I work across several repos and kept restarting ghaw or using s to manually switch. So I added tabs.
Each tab holds its own independent state -- runs, selected index, detail view, scroll position. Polling fetches runs for all open tabs in parallel, so background tabs stay current. The tab bar shows at the top when you have more than one repo open:
1: dzoba/github-actions-watcher 2: dzoba/dzobadotcom 3: dzoba/finch-app
Switch with 1-9 number keys or tab/shift-tab to cycle. w closes the current tab.
The old text input for switching repos is replaced by a picker (s key). It fetches your 20 most recently-pushed repos via gh repo list, shows them in a filterable list, and marks already-open repos with *. Type to narrow the list, or type a full owner/repo that isn't listed to add it manually. x removes a tab from the picker view.
I also added a progress indicator for in-progress runs. The time column in the list view now shows a ticking elapsed duration in yellow (e.g. 2m 31s) instead of the relative created-at time. In the detail view, in-progress steps now show their running duration too -- previously they were blank because the code required completedAt to be set.
Stack
- Bubbletea for the TUI framework
- Lipgloss for styling
- Go -- single binary, cross-compiled for macOS/Linux/Windows
- gh CLI for all GitHub data
- GoReleaser for builds, releases, and Homebrew tap
Distribution war stories
Publishing to npm from GitHub Actions with 2FA enabled was the first headache. Granular access tokens scoped to a specific package can't publish that package if it doesn't exist on npm yet. You need an "All packages" scoped token for the initial publish, then you can lock it down.
GoReleaser had its own gotcha. By default it uses GITHUB_TOKEN for everything, but that token can't push to a different repo (the Homebrew tap). You need a separate PAT with write access to the tap repo, passed as HOMEBREW_TAP_GITHUB_TOKEN, and explicitly referenced in the goreleaser config:
brews:
- name: ghaw
repository:
owner: dzoba
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
If you re-run a failed release, GoReleaser will 422 trying to re-upload assets that already exist from the first run. Delete the release first, then re-run.
Install
# Homebrew
brew install dzoba/tap/ghaw
# Go
go install github.com/dzoba/github-actions-watcher/cmd/ghaw@latest
# Or grab a binary from GitHub Releases