From 76361e2f05b330c340f4dff73eea98be882f3e4f Mon Sep 17 00:00:00 2001 From: Kristoffer Risa Date: Fri, 28 Aug 2026 16:49:36 +0000 Subject: [PATCH] docs: correct the architecture doc against the code and link the badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARCHITECTURE.md had drifted from the implementation. Verified each claim against the source rather than editing around it: - WebSocket client claimed "Reconnection: Exponential backoff (max 5 retries)" and "Heartbeat: 30-second ping interval". Neither exists — there is no reconnect, backoff, retry or ping anywhere in internal/. README repeated the same claim in Troubleshooting. Both now say what actually happens: the stream ends and exits 1. - "No Pulse | Exit 2" — nothing in the codebase exits 2. Every error path goes through exitWithError, which exits 1. - Dependency table listed nhooyr/websocket; go.mod has coder/websocket v1.8.15. - Formatter interface was missing FormatConsumptionHistory. - Directory tree was missing consumption.go, every _test.go file, CONTEXT.md and .goreleaser.yml. - The build-all snippet showed a platform loop the Makefile does not have. - Testing Strategy omitted websocket_test.go. - Title said "Tibber CLI"; the project, module and binary are powerctl. Also documents the release pipeline, which was undocumented in both ARCHITECTURE.md and CONTRIBUTING.md despite being the only way to ship. README badges were bare images that linked nowhere. They are now links, the tests badge is pinned to main, and a release-notes badge points at the releases page. All four URLs verified to return 200 with live values. --- ARCHITECTURE.md | 62 +++++++++++++++++++++++++++++++++++++------------ CONTRIBUTING.md | 33 ++++++++++++++++++++++++++ README.md | 10 ++++---- 3 files changed, 86 insertions(+), 19 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 34efb36..56f5893 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# Tibber CLI Architecture +# powerctl Architecture A cross-platform CLI tool for Tibber power consumption data, built following Unix philosophy. @@ -19,28 +19,35 @@ powerctl-cli/ ├── internal/ │ ├── api/ │ │ ├── client.go # GraphQL HTTP client +│ │ ├── client_test.go │ │ ├── queries.go # GraphQL query definitions -│ │ └── websocket.go # WebSocket for live streaming +│ │ ├── websocket.go # WebSocket for live streaming +│ │ └── websocket_test.go │ ├── commands/ │ │ ├── root.go # Root command, global flags │ │ ├── config.go # `powerctl config` - setup wizard │ │ ├── home.go # `powerctl home` │ │ ├── prices.go # `powerctl prices` +│ │ ├── consumption.go # `powerctl consumption` │ │ ├── live.go # `powerctl live` │ │ └── version.go # `powerctl version` │ ├── config/ -│ │ └── config.go # Configuration loading +│ │ ├── config.go # Configuration loading +│ │ └── config_test.go │ ├── models/ │ │ └── types.go # Data structures │ └── output/ │ ├── formatter.go # Formatter interface +│ ├── formatter_test.go │ ├── pretty.go # Beautiful CLI output (default) │ ├── json.go # JSON formatter │ └── markdown.go # Markdown formatter +├── .goreleaser.yml # Build, release and Homebrew cask publishing ├── go.mod ├── go.sum ├── Makefile ├── README.md +├── CONTEXT.md # Domain vocabulary ├── CONTRIBUTING.md └── ARCHITECTURE.md ``` @@ -86,9 +93,12 @@ format: "markdown" # Default output format #### WebSocket Client (`websocket.go`) - Protocol: `graphql-transport-ws` -- Reconnection: Exponential backoff (max 5 retries) -- Heartbeat: 30-second ping interval +- Requires `User-Agent: powerctl-cli/1.0` — Tibber rejects the default Go client +- Reconnection: none. A read or parse error ends the stream and exits 1 +- Heartbeat: none. The connection lives as long as the server keeps it open - Graceful shutdown on SIGINT/SIGTERM +- A GraphQL error payload or a null `liveMeasurement` becomes an error rather + than a nil measurement handed to the formatter (see #13) ### Commands (`internal/commands/`) @@ -111,6 +121,7 @@ type Formatter interface { FormatHomes(homes []models.HomeResponse) string FormatPrices(prices *models.PriceInfo, homeID string) string FormatLiveMeasurement(m *models.LiveMeasurement) string + FormatConsumptionHistory(nodes []models.ConsumptionNode, resolution string) string } ``` @@ -170,28 +181,27 @@ Authorization: Bearer |----------|----------| | Network errors | Log and exit with code 1 | | Auth errors | Clear message: "Invalid token" | -| No Pulse | Exit 2 with "Pulse not enabled" | +| No Pulse | Exit 1 with "Pulse not enabled" | | Parse errors | Log raw response, exit 1 | ## Cross-Platform Build -```makefile -PLATFORMS := linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 +`make build-all` fans out to per-OS targets: -build-all: - @for platform in $(PLATFORMS); do \ - GOOS=$${platform%/*} GOARCH=$${platform#*/} \ - go build -o dist/powerctl-$${platform%/*}-$${platform#*/} ./cmd/powerctl; \ - done +```makefile +build-all: build-linux build-darwin build-windows ``` +covering linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 and +windows/amd64. Release builds do not use this — see below. + ## Dependencies | Package | Purpose | Why | |---------|---------|-----| | `spf13/cobra` | CLI framework | Industry standard (kubectl, hugo) | | `spf13/viper` | Config loading | Handles env + file + flags | -| `nhooyr/websocket` | WebSocket | Pure Go, well maintained | +| `coder/websocket` | WebSocket | Pure Go, well maintained (formerly `nhooyr.io/websocket`) | | `gopkg.in/yaml.v3` | Config file writing | Used by `config init`/`config set` | ## Testing Strategy @@ -199,15 +209,37 @@ build-all: ``` internal/ ├── api/ -│ └── client_test.go # Mock HTTP responses +│ ├── client_test.go # Mock HTTP responses +│ └── websocket_test.go # Live payload parsing and error mapping ├── config/ │ └── config_test.go # Config resolution order └── output/ └── formatter_test.go # Formatter output assertions ``` +`make check` runs the same gates as CI: `gofmt` check, `go vet`, `go mod verify` +and `go test -race ./...`. + Demo token for testing: `5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE` +## Release Pipeline + +Tagging `v*.*.*` triggers `.github/workflows/release.yml`, which runs the test +suite and then GoReleaser: + +1. Cross-compiles six targets — linux, darwin and windows on amd64 and arm64. + These are the release binaries, not `make build-all`. +2. Archives them (`tar.gz`, `zip` on windows) and writes `checksums.txt`. +3. Creates the GitHub release, with grouped notes and an install footer built + from the `changelog` and `release` blocks in `.goreleaser.yml`. +4. Generates `Casks/powerctl.rb` and pushes it to + [KristofferRisa/homebrew-powerctl](https://github.com/KristofferRisa/homebrew-powerctl), + authenticated with the `HOMEBREW_TAP_TOKEN` secret. + +The tap is generated output — never hand-edit the cask; it is overwritten by +the next release. `replace_existing_artifacts: true` lets a failed release job +be re-run without colliding with assets the first attempt already uploaded. + ## Security Considerations 1. Token never logged or printed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1393482..730b095 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -164,6 +164,39 @@ Make sure: CI (lint, test, build) must be green before merge. +## Releasing + +Maintainers only. Releases are cut from a tag — there is no manual build or +upload step, and the Homebrew tap is never edited by hand. + +```bash +git checkout main && git pull +git tag -a v0.4.3 -m "v0.4.3" +git push origin v0.4.3 +``` + +That triggers `.github/workflows/release.yml`, which runs the tests and then +GoReleaser: cross-compiles six targets, publishes the GitHub release with +grouped notes, and pushes the generated cask to +[KristofferRisa/homebrew-powerctl](https://github.com/KristofferRisa/homebrew-powerctl). + +Before tagging, it is worth a dry run — it renders the release notes without +publishing anything: + +```bash +goreleaser check +goreleaser release --clean --skip=publish,announce +cat dist/CHANGELOG.md +``` + +Notes are grouped from commit subjects. Conventional prefixes (`feat:`, `fix:`) +land in their sections; anything else falls into "Other changes" rather than +being dropped. `docs:`, `test:`, `chore:`, `ci:` and `style:` are filtered out, +with or without a scope. + +If a release job fails partway, re-run it — `replace_existing_artifacts: true` +lets it overwrite assets the first attempt already uploaded. + ## AI-assisted contributions Using AI tooling to write your contribution is fine — this project is built with diff --git a/README.md b/README.md index 005f362..aa4141f 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,10 @@ A beautiful command-line tool for monitoring your Tibber power consumption and electricity prices.

- Release - Tests - License + Latest release + Release notes + Tests + License

## Features @@ -247,7 +248,8 @@ make lint # Run linter (requires golangci-lint) **Live stream disconnects** - Rate limit is 20 connections/hour -- WebSocket auto-reconnects on temporary failures +- There is no automatic reconnect — the command exits with code 1 and the error. + Re-run it, or wrap it in a supervisor if you need it to stay up ## Contributing