Skip to content

Commit 5716c32

Browse files
committed
feat: update golangci-lint version and improve lint handling in CI
fix: change skip-dirs to exclude-dirs in golangci configuration chore: remove deprecated .openboot.yml and .openboot.yml.example files docs: update CHANGELOG for command removals and changes in v1.0 docs: enhance CLAUDE.md with project structure and guidelines feat: add context support to LoginInteractive and related functions test: refactor login tests to use context and improve coverage fix: improve error handling in GetInstalledPackages feat: implement context handling in CLI commands for better cancellation test: enhance snapshot tests to verify health records for failed steps refactor: streamline API URL validation to prevent prefix-bypass attacks chore: add quality score JSON file for tracking code quality metrics
1 parent 711737f commit 5716c32

24 files changed

Lines changed: 242 additions & 217 deletions

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: golangci-lint
3232
uses: golangci/golangci-lint-action@v6
3333
with:
34-
version: latest
34+
version: v2.11.4
3535

3636
unit:
3737
name: unit (L1)

.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: "2"
22

33
run:
44
timeout: 5m
5-
skip-dirs:
5+
exclude-dirs:
66
- vendor
77
- testutil
88

.openboot.yml

Lines changed: 0 additions & 18 deletions
This file was deleted.

.openboot.yml.example

Lines changed: 0 additions & 34 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ Six commands are removed outright. Each prints an error with a migration hint wh
2121
| `openboot doctor` | **no replacement** — use `brew doctor` and `git config --list` directly |
2222
| `openboot update` | **no replacement** — use `brew upgrade` directly; OpenBoot self-updates on launch |
2323

24-
Three flat commands move under a `config` namespace:
24+
Three flat commands are removed with no replacement — manage configs directly at openboot.dev:
2525

26-
| Before | After |
27-
|--------|-------|
28-
| `openboot list` | `openboot config list` |
29-
| `openboot edit` | `openboot config edit` |
30-
| `openboot delete` | `openboot config delete` |
26+
| Command | Status |
27+
|---------|--------|
28+
| `openboot list` | **no replacement** — use openboot.dev dashboard |
29+
| `openboot edit` | **no replacement** — use openboot.dev dashboard |
30+
| `openboot delete` | **no replacement** — use openboot.dev dashboard |
3131

3232
No aliases are kept — silent aliasing would regress behavior invisibly (the old `pull` did uninstalls, the new `install` does not).
3333

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ make clean
4141
cmd/openboot/ # main.go → cli.Execute()
4242
internal/
4343
auth/ # OAuth-like login, token in ~/.openboot/auth.json (0600)
44-
brew/ # Homebrew ops, parallel workers (4 max), retry, uninstall
44+
brew/ # Homebrew ops, sequential install with retry, uninstall
4545
cli/ # Cobra cmds: install, snapshot, login, logout, version
4646
config/ # Package catalog + presets + remote fetch (embed fallback in data/)
4747
diff/ # Pure-logic system-vs-config comparison
@@ -94,7 +94,7 @@ These cannot be inferred from code alone — everything else is enforced by `go
9494
- **Destructive ops**: check `cfg.DryRun` first. Always.
9595
- **Paths**: `os.UserHomeDir()` — never hardcode `~` or `/Users/...`.
9696
- **State**: everything user-local goes under `~/.openboot/` (auth, cache, snapshots, state).
97-
- **Concurrency**: bounded `sync.WaitGroup` — brew uses max 4 workers. No unbounded goroutines.
97+
- **Concurrency**: bounded `sync.WaitGroup` — brew install is sequential with retry; `GetInstalledPackages` uses 2 goroutines for formula+cask list. No unbounded goroutines.
9898
- **Embedded data**: `//go:embed data/*.yaml` loaded in `init()`.
9999
- **Tests**: table-driven, `testify/require` for fatal, `testify/assert` for non-fatal. L1 uses the `Runner` interface to fake subprocess calls — no real network, no real fork.
100100
- **Commits**: Conventional (`feat:` / `fix:` / `docs:` / `refactor:` / `test:` / `chore:` / `ci:`), one thing per commit.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ Removed in v1.0: `pull`, `push`, `diff`, `clean`, `log`, `restore`, `init`, `set
180180
```
181181
-p, --preset NAME Set preset (minimal, developer, full)
182182
-u, --user NAME Use alias or openboot.dev username/slug config
183+
--from FILE Install from a local config or snapshot JSON file
183184
-s, --silent Non-interactive mode (requires env vars)
184185
--dry-run Preview what would be installed
185186
--packages-only Install packages only, skip system config
@@ -188,6 +189,7 @@ Removed in v1.0: `pull`, `push`, `diff`, `clean`, `log`, `restore`, `init`, `set
188189
--macos MODE macOS prefs: configure, skip
189190
--dotfiles MODE Dotfiles: clone, link, skip
190191
--post-install MODE Post-install script: skip
192+
--allow-post-install Allow post-install scripts in silent mode
191193
```
192194

193195
</details>

internal/auth/login.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package auth
22

33
import (
4+
"context"
45
"crypto/tls"
56
"encoding/json"
67
"fmt"
@@ -51,7 +52,7 @@ type cliPollResponse struct {
5152
ExpiresAt string `json:"expires_at,omitempty"`
5253
}
5354

54-
func LoginInteractive(apiBase string) (*StoredAuth, error) {
55+
func LoginInteractive(ctx context.Context, apiBase string) (*StoredAuth, error) {
5556
codeID, code, err := startAuthSession(apiBase)
5657
if err != nil {
5758
return nil, err
@@ -69,7 +70,7 @@ func LoginInteractive(apiBase string) (*StoredAuth, error) {
6970

7071
fmt.Fprintf(os.Stderr, "\nWaiting for approval...\n")
7172

72-
result, err := pollForApproval(apiBase, codeID)
73+
result, err := pollForApproval(ctx, apiBase, codeID)
7374
if err != nil {
7475
return nil, err
7576
}
@@ -135,14 +136,16 @@ var (
135136
pollInterval = 2 * time.Second
136137
)
137138

138-
func pollForApproval(apiBase, codeID string) (*cliPollResponse, error) {
139+
func pollForApproval(ctx context.Context, apiBase, codeID string) (*cliPollResponse, error) {
139140
pollURL := fmt.Sprintf("%s/api/auth/cli/poll?code_id=%s", apiBase, url.QueryEscape(codeID))
140141
timeout := time.After(pollTimeout)
141142
ticker := time.NewTicker(pollInterval)
142143
defer ticker.Stop()
143144

144145
for {
145146
select {
147+
case <-ctx.Done():
148+
return nil, fmt.Errorf("login cancelled")
146149
case <-timeout:
147150
return nil, fmt.Errorf("authentication timed out after 5 minutes")
148151
case <-ticker.C:

internal/auth/login_test.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package auth
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"io"
@@ -204,7 +205,7 @@ func TestPollForApproval_Approved(t *testing.T) {
204205
json.NewEncoder(w).Encode(resp) //nolint:errcheck // test helper
205206
}))
206207

207-
result, err := pollForApproval(fakeBase+"/poll", "code_id_123")
208+
result, err := pollForApproval(context.Background(), fakeBase+"/poll", "code_id_123")
208209
require.NoError(t, err)
209210
assert.NotNil(t, result)
210211
assert.Equal(t, "approved", result.Status)
@@ -222,7 +223,7 @@ func TestPollForApproval_Expired(t *testing.T) {
222223
json.NewEncoder(w).Encode(resp) //nolint:errcheck // test helper
223224
}))
224225

225-
result, err := pollForApproval(fakeBase+"/poll", "code_id_123")
226+
result, err := pollForApproval(context.Background(), fakeBase+"/poll", "code_id_123")
226227
assert.Error(t, err)
227228
assert.Nil(t, result)
228229
assert.Contains(t, err.Error(), "authorization code expired")
@@ -250,7 +251,7 @@ func TestPollForApproval_Pending(t *testing.T) {
250251
}
251252
}))
252253

253-
result, err := pollForApproval(fakeBase+"/poll", "code_id_123")
254+
result, err := pollForApproval(context.Background(), fakeBase+"/poll", "code_id_123")
254255
require.NoError(t, err)
255256
assert.NotNil(t, result)
256257
assert.Equal(t, "approved", result.Status)
@@ -274,7 +275,7 @@ func TestPollForApproval_TimeoutBehavior(t *testing.T) {
274275
}))
275276

276277
start := time.Now()
277-
result, err := pollForApproval(fakeBase+"/poll", "code_id_123")
278+
result, err := pollForApproval(context.Background(), fakeBase+"/poll", "code_id_123")
278279
elapsed := time.Since(start)
279280

280281
assert.Error(t, err)
@@ -299,7 +300,7 @@ func TestPollForApproval_InvalidResponse(t *testing.T) {
299300
w.Write([]byte("invalid json {")) //nolint:errcheck // test helper
300301
}))
301302

302-
result, err := pollForApproval(fakeBase+"/poll", "code_id_123")
303+
result, err := pollForApproval(context.Background(), fakeBase+"/poll", "code_id_123")
303304
assert.Error(t, err)
304305
assert.Nil(t, result)
305306
assert.Contains(t, err.Error(), "timed out")
@@ -406,7 +407,7 @@ func TestLoginInteractive_SuccessRFC3339(t *testing.T) {
406407
}
407408
}))
408409

409-
auth, err := LoginInteractive(fakeBase)
410+
auth, err := LoginInteractive(context.Background(), fakeBase)
410411
require.NoError(t, err)
411412
assert.NotNil(t, auth)
412413
assert.Equal(t, "obt_token_123", auth.Token)
@@ -442,7 +443,7 @@ func TestLoginInteractive_SuccessSQLiteFormat(t *testing.T) {
442443
}
443444
}))
444445

445-
auth, err := LoginInteractive(fakeBase)
446+
auth, err := LoginInteractive(context.Background(), fakeBase)
446447
require.NoError(t, err)
447448
assert.NotNil(t, auth)
448449
assert.Equal(t, "obt_token_123", auth.Token)
@@ -458,7 +459,7 @@ func TestLoginInteractive_StartAuthSessionError(t *testing.T) {
458459
w.WriteHeader(http.StatusInternalServerError)
459460
}))
460461

461-
auth, err := LoginInteractive(fakeBase)
462+
auth, err := LoginInteractive(context.Background(), fakeBase)
462463
assert.Error(t, err)
463464
assert.Nil(t, auth)
464465
assert.Contains(t, err.Error(), "status 500")
@@ -483,7 +484,7 @@ func TestLoginInteractive_PollForApprovalError(t *testing.T) {
483484
}
484485
}))
485486

486-
auth, err := LoginInteractive(fakeBase)
487+
auth, err := LoginInteractive(context.Background(), fakeBase)
487488
assert.Error(t, err)
488489
assert.Nil(t, auth)
489490
assert.Contains(t, err.Error(), "authorization code expired")
@@ -513,7 +514,7 @@ func TestLoginInteractive_InvalidExpirationFormat(t *testing.T) {
513514
}
514515
}))
515516

516-
auth, err := LoginInteractive(fakeBase)
517+
auth, err := LoginInteractive(context.Background(), fakeBase)
517518
assert.Error(t, err)
518519
assert.Nil(t, auth)
519520
assert.Contains(t, err.Error(), "parse expiration")
@@ -548,7 +549,7 @@ func TestLoginInteractive_SaveTokenError(t *testing.T) {
548549
}
549550
}))
550551

551-
auth, err := LoginInteractive(fakeBase)
552+
auth, err := LoginInteractive(context.Background(), fakeBase)
552553
assert.Error(t, err)
553554
assert.Nil(t, auth)
554555
assert.Contains(t, err.Error(), "save auth token")

internal/brew/brew.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ func GetInstalledPackages() (formulae map[string]bool, casks map[string]bool, er
4949
wg.Wait()
5050

5151
if fErr != nil {
52-
return nil, nil, fErr
52+
return nil, nil, fmt.Errorf("list formulae: %w", fErr)
5353
}
5454
if cErr != nil {
55-
return nil, nil, cErr
55+
return nil, nil, fmt.Errorf("list casks: %w", cErr)
5656
}
5757

5858
for _, name := range strings.Split(strings.TrimSpace(string(fOut)), "\n") {

0 commit comments

Comments
 (0)