Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
89 changes: 89 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Ready to Review is a macOS/Linux/Windows menubar application that helps developers track GitHub pull requests. It shows a count of incoming/outgoing PRs and highlights when you're blocking someone's review. The app integrates with the Turn API to provide intelligent PR metadata about who's actually blocking progress.

## Key Commands

### Building and Running
```bash
make run # Build and run (on macOS: installs to /Applications and launches)
make build # Build for current platform
make app-bundle # Create macOS .app bundle
make install # Install to system (macOS: /Applications, Linux: /usr/local/bin, Windows: %LOCALAPPDATA%)
```

### Development
```bash
make lint # Run all linters (golangci-lint with strict config + yamllint)
make fix # Auto-fix linting issues where possible
make deps # Download and tidy Go dependencies
make clean # Remove build artifacts
```

## Architecture Overview

### Core Components

1. **Main Application Flow** (`main.go`)
- Single `context.Background()` created in main, passed through all functions
- App struct holds GitHub/Turn clients, PR data, and UI state
- Update loop runs every 2 minutes to refresh PR data
- Menu updates only rebuild when PR data actually changes (hash-based optimization)

2. **GitHub Integration** (`github.go`)
- Uses GitHub CLI token (`gh auth token`) for authentication
- Fetches PRs with a single optimized query: `is:open is:pr involves:USER archived:false`
- No pagination needed (uses 100 per page limit)

3. **Turn API Integration** (`cache.go`)
- Provides intelligent PR metadata (who's blocking, PR size, tags)
- Implements 2-hour TTL cache with SHA256-based cache keys
- Cache cleanup runs daily, removes files older than 5 days
- Turn API calls are made for each PR to determine blocking status

4. **UI Management** (`ui.go`)
- System tray integration via energye/systray
- Menu structure: Incoming PRs → Outgoing PRs → Settings → About
- Click handlers open PRs in browser with URL validation
- "Hide stale PRs" option filters PRs older than 90 days

5. **Platform-Specific Features**
- `loginitem_darwin.go`: macOS "Start at Login" functionality via AppleScript
- `loginitem_other.go`: Stub for non-macOS platforms

### Key Design Decisions

- **No Context in Structs**: Context is always passed as a parameter, never stored
- **Graceful Degradation**: Turn API failures don't break the app, PRs still display
- **Security**: Only HTTPS URLs allowed, whitelist of github.com and dash.ready-to-review.dev
- **Minimal Dependencies**: Uses standard library where possible
- **Proper Cancellation**: All goroutines respect context cancellation

### Linting Configuration

The project uses an extremely strict golangci-lint configuration (`.golangci.yml`) that enforces:
- All available linters except those that conflict with Go best practices
- No nolint directives without explanations
- Cognitive complexity limit of 55
- No magic numbers (must use constants)
- Proper error handling (no unchecked errors)
- No naked returns except in very short functions
- Field alignment optimization for structs

### Special Considerations

1. **Authentication**: Uses GitHub CLI token, never stores it persistently
2. **Caching**: Turn API responses cached to reduce API calls
3. **Menu Updates**: Hash-based change detection prevents unnecessary UI updates
4. **Context Handling**: Single context from main, proper cancellation in all goroutines
5. **Error Handling**: All errors wrapped with context using `fmt.Errorf` with `%w`

When making changes:
- Run `make lint` and fix all issues without adding nolint directives
- Follow the strict Go style guidelines in ~/.claude/CLAUDE.md
- Keep functions simple and focused
- Test macOS-specific features carefully (login items, app bundle)
24 changes: 0 additions & 24 deletions .claude/prompt.txt

This file was deleted.

16 changes: 16 additions & 0 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Security Policy

## Reporting a Vulnerability

Please follow our security reporting guidelines at:
https://github.com/codeGROOVE-dev/vulnerability-reports/blob/main/SECURITY.md

This document contains all the specifics for how to submit a security report, including contact information, expected response times, and disclosure policies.

## Security Practices

- GitHub tokens are never logged or stored
- All inputs are validated
- File permissions are restricted (0600/0700)
- Only HTTPS URLs to github.com are allowed
- No shell interpolation of user data
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

on:
pull_request:
branches: [ main ]
push:
branches: [ main ]

permissions:
contents: read

jobs:
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go-version: ['1.21']

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true

- name: Install dependencies (Linux)
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get update && sudo apt-get install -y gcc libgl1-mesa-dev xorg-dev

- name: Lint
if: matrix.os != 'windows-latest'
run: make lint

- name: Build
run: make build

- name: Test
run: go test -v -race ./...
28 changes: 15 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
LDFLAGS := -X main.version=$(GIT_VERSION) -X main.commit=$(GIT_COMMIT) -X main.date=$(BUILD_DATE)

.PHONY: build clean deps run app-bundle install install-darwin install-unix install-windows
.PHONY: all build clean deps run app-bundle install install-darwin install-unix install-windows

# Default target
all: build

# Install dependencies
deps:
Expand All @@ -24,39 +27,38 @@ ifeq ($(shell uname),Darwin)
@echo "Running $(BUNDLE_NAME) from /Applications..."
@open "/Applications/$(BUNDLE_NAME).app"
else
go run .
go run ./cmd/goose
endif

# Build for current platform
build:
build: out
ifeq ($(OS),Windows_NT)
CGO_ENABLED=1 go build -ldflags "-H=windowsgui $(LDFLAGS)" -o $(APP_NAME).exe .
CGO_ENABLED=1 go build -ldflags "-H=windowsgui $(LDFLAGS)" -o out/$(APP_NAME).exe ./cmd/goose
else
CGO_ENABLED=1 go build -ldflags "$(LDFLAGS)" -o $(APP_NAME) .
CGO_ENABLED=1 go build -ldflags "$(LDFLAGS)" -o out/$(APP_NAME) ./cmd/goose
endif

# Build for all platforms
build-all: build-darwin build-linux build-windows

# Build for macOS
build-darwin:
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o out/$(APP_NAME)-darwin-amd64 .
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o out/$(APP_NAME)-darwin-arm64 .
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o out/$(APP_NAME)-darwin-amd64 ./cmd/goose
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o out/$(APP_NAME)-darwin-arm64 ./cmd/goose

# Build for Linux
build-linux:
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o out/$(APP_NAME)-linux-amd64 .
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o out/$(APP_NAME)-linux-arm64 .
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o out/$(APP_NAME)-linux-amd64 ./cmd/goose
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o out/$(APP_NAME)-linux-arm64 ./cmd/goose

# Build for Windows
build-windows:
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui $(LDFLAGS)" -o out/$(APP_NAME)-windows-amd64.exe .
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui $(LDFLAGS)" -o out/$(APP_NAME)-windows-amd64.exe ./cmd/goose
CGO_ENABLED=1 GOOS=windows GOARCH=arm64 go build -ldflags "-H=windowsgui $(LDFLAGS)" -o out/$(APP_NAME)-windows-arm64.exe .

# Clean build artifacts
clean:
rm -rf out/
rm -f $(APP_NAME)

# Create out directory
out:
Expand Down Expand Up @@ -163,7 +165,7 @@ install-darwin: app-bundle
install-unix: build
@echo "Installing on $(shell uname)..."
@echo "Installing binary to /usr/local/bin..."
@sudo install -m 755 $(APP_NAME) /usr/local/bin/
@sudo install -m 755 out/$(APP_NAME) /usr/local/bin/
@echo "Installation complete! $(APP_NAME) has been installed to /usr/local/bin"

# Install on Windows
Expand All @@ -172,7 +174,7 @@ install-windows: build
@echo "Creating program directory..."
@if not exist "%LOCALAPPDATA%\Programs\$(APP_NAME)" mkdir "%LOCALAPPDATA%\Programs\$(APP_NAME)"
@echo "Copying executable..."
@copy /Y "$(APP_NAME).exe" "%LOCALAPPDATA%\Programs\$(APP_NAME)\"
@copy /Y "out\$(APP_NAME).exe" "%LOCALAPPDATA%\Programs\$(APP_NAME)\"
@echo "Installation complete! $(APP_NAME) has been installed to %LOCALAPPDATA%\Programs\$(APP_NAME)"
@echo "You may want to add %LOCALAPPDATA%\Programs\$(APP_NAME) to your PATH environment variable."
# BEGIN: lint-install .
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ The only PR tracker that honks at you when you're the bottleneck. Now shipping w

Lives in your menubar like a tiny waterfowl of productivity shame, watching your GitHub PRs and making aggressive bird sounds when you're blocking someone's code from seeing the light of production.

> ⚠️ **EXPERIMENTAL**: This is very much a work in progress. The blocking logic has bugs. It theoretically runs on Linux, BSD, and Windows but we've literally never tested it there. Here be dragons (and geese).
> ⚠️ **EXPERIMENTAL**: This is very much a work in progress. The blocking logic has bugs. Linux/BSD/Windows support is untested. Here be dragons (and geese).

![PR Menubar Screenshot](media/screenshot.png)

## What It Does

- **🪿 Honks** when you're blocking someone's PR (authentic goose noises included)
- **🚀 Rocket sounds** when own your own PR is ready to go to the next stage
- **✈️ Jet sounds** when your own PR is ready for the next stage
- **🧠 Smart turn-based assignment** - knows who is blocking a PR, knows when tests are failing, etc.
- **⭐ Auto-start** on login (macOS)

Expand Down Expand Up @@ -71,7 +71,6 @@ When `GITHUB_TOKEN` is set, the goose will use it directly instead of the GitHub
## Known Issues

- Blocking logic isn't 100% accurate (we're working on it)
- Linux/BSD/Windows support likely works, but remains untested
- The goose may not stop honking until you review your PRs
- Visual notifications won't work on macOS until we sign the binary

Expand All @@ -83,9 +82,9 @@ This tool is part of the [CodeGroove](https://codegroove.dev) developer accelera

## Privacy

- Your GitHub token is used to fetch PR metadata, but is never stored or logged.
- We won't sell your information or use it for any purpose other than caching.
- GitHub metadata for open pull requests may be cached for up to 20 days for performance reasons.
- Your GitHub token is never stored or logged
- PR metadata cached for up to 20 days (performance)
- No telemetry or external data collection

---

Expand Down
Binary file renamed media/.DS_Store → cmd/goose/.DS_Store
Binary file not shown.
File renamed without changes.
40 changes: 17 additions & 23 deletions github.go → cmd/goose/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,19 @@ func (app *App) initClients(ctx context.Context) error {

// token retrieves the GitHub token from GITHUB_TOKEN env var or gh CLI.
func (*App) token(ctx context.Context) (string, error) {
// First check for GITHUB_TOKEN environment variable
// Check GITHUB_TOKEN environment variable first
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
token = strings.TrimSpace(token)
if err := validateGitHubToken(token); err != nil {
return "", fmt.Errorf("invalid GITHUB_TOKEN: %w", err)
// Validate token format inline
if token == "" {
return "", errors.New("GITHUB_TOKEN is empty")
}
if !githubTokenRegex.MatchString(token) {
return "", errors.New("GITHUB_TOKEN has invalid format")
}
log.Println("Using GitHub token from GITHUB_TOKEN environment variable")
return token, nil
}

// Fall back to gh CLI if GITHUB_TOKEN is not set
// Only check absolute paths for security - never use PATH
var trustedPaths []string
switch runtime.GOOS {
Expand Down Expand Up @@ -189,10 +191,19 @@ func (app *App) executeGitHubQuery(ctx context.Context, query string, opts *gith
return result, nil
}

// prResult holds the result of a Turn API query for a PR.
type prResult struct {
err error
turnData *turn.CheckResponse
url string
isOwner bool
wasFromCache bool
}

// fetchPRsInternal is the implementation for PR fetching.
// It returns GitHub data immediately and starts Turn API queries in the background (when waitForTurn=false),
// or waits for Turn data to complete (when waitForTurn=true).
func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incoming []PR, outgoing []PR, err error) {
func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incoming []PR, outgoing []PR, _ error) {
// Use targetUser if specified, otherwise use authenticated user
user := app.currentUser.GetLogin()
if app.targetUser != "" {
Expand Down Expand Up @@ -376,13 +387,6 @@ func (app *App) updatePRData(url string, needsReview bool, isOwner bool, actionR
// fetchTurnDataSync fetches Turn API data synchronously and updates PRs directly.
func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, user string, incoming *[]PR, outgoing *[]PR) {
turnStart := time.Now()
type prResult struct {
err error
turnData *turn.CheckResponse
url string
isOwner bool
wasFromCache bool
}

// Create a channel for results
results := make(chan prResult, len(issues))
Expand Down Expand Up @@ -479,17 +483,7 @@ func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, u

// fetchTurnDataAsync fetches Turn API data in the background and updates PRs as results arrive.
func (app *App) fetchTurnDataAsync(ctx context.Context, issues []*github.Issue, user string) {
// Log start of Turn API queries
// Start Turn API queries in background

turnStart := time.Now()
type prResult struct {
err error
turnData *turn.CheckResponse
url string
isOwner bool
wasFromCache bool
}

// Create a channel for results
results := make(chan prResult, len(issues))
Expand Down
File renamed without changes.
5 changes: 2 additions & 3 deletions loginitem_other.go → cmd/goose/loginitem_other.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
//go:build !darwin
// +build !darwin

// Package main - loginitem_other.go provides stub functions for non-macOS platforms.
package main

import "context"

// addLoginItemUI is a no-op on non-macOS platforms
func addLoginItemUI(ctx context.Context, app *App) {
// addLoginItemUI is a no-op on non-macOS platforms.
func addLoginItemUI(_ context.Context, _ *App) {
// Login items are only supported on macOS
}
Loading
Loading