switched out make for just

This commit is contained in:
2026-02-13 13:27:14 +11:00
parent 6e779fa560
commit ea8b74c5e3
9 changed files with 470 additions and 477 deletions

405
AGENTS.md
View File

@@ -9,78 +9,62 @@ This document provides guidelines for AI coding agents and developers working on
**Architecture**: Web application with Discord OAuth, PostgreSQL database, templ templates
**Key Technologies**: Bun ORM, templ, TailwindCSS, custom golib libraries
## Build, Test, and Development Commands
## Build and Development Commands
### Building
NEVER BUILD MANUALLY
```bash
# Full production build (tailwind → templ → go generate → go build)
make build
just build
# Build and run
make run
# Clean build artifacts
make clean
just run
```
### Development Mode
```bash
# Watch mode with hot reload (templ, air, tailwindcss in parallel)
make dev
just dev
# Development server runs on:
# - Proxy: http://localhost:3000 (use this)
# - App: http://localhost:3333 (internal)
```
### Testing
```bash
# Run all tests
go test ./...
# Run tests for a specific package
go test ./pkg/oauth
# Run a single test function
go test ./pkg/oauth -run TestGenerateState_Success
# Run tests with verbose output
go test -v ./pkg/oauth
# Run tests with coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
```
### Database Migrations
**oslstats uses Bun's migration framework for safe, incremental schema changes.**
#### Quick Reference
**New Migration System**: Migrations now accept a count parameter. Default is 1 migration at a time.
```bash
# Show migration status
make migrate-status
just migrate status
# Preview pending migrations (dry-run)
make migrate-dry-run
# Run 1 migration (default, with automatic backup)
just migrate up 1
# OR just
just migrate up
# Run pending migrations (with automatic backup)
make migrate
# Run 3 migrations
just migrate up 3
# Rollback last migration group
make migrate-rollback
# Run all pending migrations
just migrate up all
# Run with a specific environment file
just migrate up 3 .test.env
# Rollback works the same for all arguments
just migrate down 2 .test.env
# Create new migration
make migrate-create NAME=add_email_to_users
# Dev: Run migrations without backup (faster)
make migrate-no-backup
just migrate new add_email_to_users
# Dev: Reset database (DESTRUCTIVE - deletes all data)
make reset-db
just reset-db
```
#### Creating a New Migration
@@ -89,9 +73,9 @@ make reset-db
1. **Generate migration file:**
```bash
make migrate-create NAME=add_email_to_users
just migrate new add_leagues_and_slap_version
```
Creates: `cmd/oslstats/migrations/20250124150030_add_email_to_users.go`
Creates: `cmd/oslstats/migrations/20250124150030_add_leagues_and_slap_version.go`
2. **Edit the migration file:**
```go
@@ -106,15 +90,55 @@ make reset-db
Migrations.MustRegister(
// UP: Add email column
func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx,
"ALTER TABLE users ADD COLUMN email VARCHAR(255)")
return err
_, err := dbConn.NewAddColumn().
Model((*db.Season)(nil)).
ColumnExpr("slap_version VARCHAR NOT NULL").
IfNotExists().
Exec(ctx)
if err != nil {
return err
}
// Create leagues table
_, err = dbConn.NewCreateTable().
Model((*db.League)(nil)).
Exec(ctx)
if err != nil {
return err
}
// Create season_leagues join table
_, err = dbConn.NewCreateTable().
Model((*db.SeasonLeague)(nil)).
Exec(ctx)
return err
},
// DOWN: Remove email column (for rollback)
func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx,
"ALTER TABLE users DROP COLUMN email")
return err
// Drop season_leagues join table first
_, err := dbConn.NewDropTable().
Model((*db.SeasonLeague)(nil)).
IfExists().
Exec(ctx)
if err != nil {
return err
}
// Drop leagues table
_, err = dbConn.NewDropTable().
Model((*db.League)(nil)).
IfExists().
Exec(ctx)
if err != nil {
return err
}
// Remove slap_version column from seasons table
_, err = dbConn.NewDropColumn().
Model((*db.Season)(nil)).
ColumnExpr("slap_version").
Exec(ctx)
return err
},
)
}
@@ -122,37 +146,26 @@ make reset-db
3. **Update the model** (`internal/db/user.go`):
```go
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
type Season struct {
bun.BaseModel `bun:"table:seasons,alias:s"`
ID int `bun:"id,pk,autoincrement"`
Username string `bun:"username,unique"`
Email string `bun:"email"` // NEW FIELD
CreatedAt int64 `bun:"created_at"`
DiscordID string `bun:"discord_id,unique"`
Name string `bun:"name,unique"`
SlapVersion string `bun:"slap_version"` // NEW FIELD
}
```
4. **Preview the migration (optional):**
4. **Apply the migration:**
```bash
make migrate-dry-run
```
Output:
```
[INFO] Pending migrations (dry-run):
📋 20250124150030_add_email_to_users
[INFO] Would migrate to group 2
```
5. **Apply the migration:**
```bash
make migrate
just migrate up 1
```
Output:
```
[INFO] Step 1/5: Validating migrations...
[INFO] Migration validation passed ✓
[INFO] Step 2/5: Checking for pending migrations...
[INFO] Running 1 migration(s):
📋 20250124150030_add_email_to_users
[INFO] Step 3/5: Creating backup...
[INFO] Backup created: backups/20250124_150145_pre_migration.sql.gz (2.3 MB)
[INFO] Step 4/5: Acquiring migration lock...
@@ -163,96 +176,6 @@ make reset-db
[INFO] Migration lock released
```
#### Migration Patterns
**Create Table:**
```go
// UP
_, err := db.NewCreateTable().
Model((*db.Game)(nil)).
Exec(ctx)
return err
// DOWN
_, err := db.NewDropTable().
Model((*db.Game)(nil)).
IfExists().
Exec(ctx)
return err
```
**Add Column:**
```go
// UP
_, err := db.ExecContext(ctx,
"ALTER TABLE users ADD COLUMN email VARCHAR(255)")
return err
// DOWN
_, err := db.ExecContext(ctx,
"ALTER TABLE users DROP COLUMN email")
return err
```
**Create Index:**
```go
// UP
_, err := db.NewCreateIndex().
Model((*db.User)(nil)).
Index("idx_username").
Column("username").
Exec(ctx)
return err
// DOWN
_, err := db.ExecContext(ctx,
"DROP INDEX IF EXISTS idx_username")
return err
```
**Data Migration:**
```go
// UP
_, err := db.NewUpdate().
Model((*db.User)(nil)).
Set("status = ?", "active").
Where("status IS NULL").
Exec(ctx)
return err
// DOWN (if reversible)
_, err := db.NewUpdate().
Model((*db.User)(nil)).
Set("status = NULL").
Where("status = ?", "active").
Exec(ctx)
return err
```
#### Safety Features
✅ **Automatic Backups**
- Compressed with gzip (saves ~80% space)
- Created before every migration and rollback
- Stored in `backups/` directory
- Retention policy keeps last 10 backups
- Graceful fallback if `pg_dump` not installed
✅ **Migration Locking**
- PostgreSQL advisory locks prevent concurrent migrations
- 5-minute timeout prevents hung locks
- Safe for multi-instance deployments
✅ **Validation**
- Pre-migration `go build` check ensures migrations compile
- Dry-run mode previews changes without applying
- Status command shows exactly what's applied
✅ **Rollback Support**
- Every migration must have a DOWN function
- Rollbacks are grouped (undo last batch together)
- Automatic backup before rollback
#### Environment Variables
```bash
@@ -289,100 +212,28 @@ DB_BACKUP_RETENTION=10
- Fix the errors and try again
- Use `go build ./cmd/oslstats/migrations` to debug
#### Best Practices
1. **Always test in development first**
- Use `make migrate-dry-run` to preview
- Test rollback: `make migrate-rollback`
- Verify with `make migrate-status`
2. **Write reversible migrations**
- DOWN function should undo UP changes
- Test rollbacks work correctly
- Some operations (data deletion) may not be reversible
3. **Keep migrations focused**
- One migration = one logical change
- Don't mix schema changes with data migrations
- Use descriptive names
4. **Production checklist**
- ✅ Tested in development
- ✅ Tested rollback
- ✅ Verified backup works
- ✅ Communicated downtime (if any)
- ✅ Have rollback plan ready
5. **Development workflow**
- Use `--no-backup` for speed: `make migrate-no-backup`
- Use `--reset-db` to start fresh (loses data!)
- Commit migrations to version control
### Configuration Management
```bash
# Generate .env template file
make genenv
# OR with custom output: make genenv OUT=.env.example
just genenv
# OR with custom output: just genenv .env.example
# Show environment variable documentation
make envdoc
just envdoc
# Show current environment values
make showenv
just showenv
```
## Code Style Guidelines
### Import Organization
Organize imports in **3 groups** separated by blank lines:
```go
import (
// 1. Standard library
"context"
"net/http"
"fmt"
// 2. External dependencies
"git.haelnorr.com/h/golib/hws"
"github.com/pkg/errors"
"github.com/uptrace/bun"
// 3. Internal packages
"git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/pkg/oauth"
)
```
### Naming Conventions
**Variables**:
- Local: `camelCase` (userAgentKey, httpServer, dbConn)
- Exported: `PascalCase` (Config, User, Token)
- Common abbreviations: `cfg`, `ctx`, `tx`, `db`, `err`, `w`, `r`
**Functions**:
- Exported: `PascalCase` (GetConfig, NewStore, GenerateState)
- Private: `camelCase` (throwError, shouldShowDetails, loadModels)
- HTTP handlers: Return `http.Handler`, use dependency injection pattern
- Database functions: Use `bun.Tx` as parameter for transactions
**Types**:
- Structs/Interfaces: `PascalCase` (Config, User, OAuthSession)
- Use `-er` suffix for interfaces (implied from usage)
**Files**:
- Prefer single word: `config.go`, `oauth.go`, `errors.go`
- Don't use snake_case except for tests: `state_test.go`
- Test files: `*_test.go` alongside source files
### Error Handling
**Always wrap errors** with context using `github.com/pkg/errors`:
```go
if err != nil {
return errors.Wrap(err, "operation_name")
return errors.Wrap(err, "package.FunctionName")
}
```
@@ -491,94 +342,14 @@ func ConfigFromEnv() (any, error) {
- Use inline comments for ENV var documentation in Config structs
- Explain security-critical code flows
### Testing
**Test File Location**: Place `*_test.go` files alongside source files
**Test Naming**:
```go
func TestFunctionName_Scenario(t *testing.T)
func TestGenerateState_Success(t *testing.T)
func TestVerifyState_WrongUserAgentKey(t *testing.T)
```
**Test Structure**:
- Use subtests with `t.Run()` for related scenarios
- Use table-driven tests for multiple similar cases
- Create helper functions for common setup (e.g., `testConfig()`)
- Test happy paths, error cases, edge cases, and security properties
**Test Categories** (from pkg/oauth/state_test.go example):
1. Happy path tests
2. Error handling (nil params, empty fields, malformed input)
3. Security tests (MITM, CSRF, replay attacks, tampering)
4. Edge cases (concurrency, constant-time comparison)
5. Integration tests (round-trip verification)
### Security
**Critical Practices**:
- Use `crypto/subtle.ConstantTimeCompare` for cryptographic comparisons
- Implement CSRF protection via state tokens
- Store sensitive cookies as HttpOnly
- Use separate logging levels for security violations (WARN)
- Validate all inputs at function boundaries
- Use parameterized queries (Bun ORM handles this)
- Never commit secrets (.env, keys/ are gitignored)
## Project Structure
```
oslstats/
├── cmd/oslstats/ # Application entry point
│ ├── main.go # Entry point with flag parsing
│ ├── run.go # Server initialization & graceful shutdown
│ ├── httpserver.go # HTTP server setup
│ ├── routes.go # Route registration
│ ├── middleware.go # Middleware registration
│ ├── auth.go # Authentication setup
│ └── db.go # Database connection & migrations
├── internal/ # Private application code
│ ├── config/ # Configuration aggregation
│ ├── db/ # Database models & queries (Bun ORM)
│ ├── discord/ # Discord OAuth integration
│ ├── handlers/ # HTTP request handlers
│ ├── session/ # Session store (in-memory)
│ └── view/ # Templ templates
│ ├── component/ # Reusable UI components
│ ├── layout/ # Page layouts
│ └── page/ # Full pages
├── pkg/ # Reusable packages
│ ├── contexts/ # Context key definitions
│ ├── embedfs/ # Embedded static files
│ └── oauth/ # OAuth state management
├── bin/ # Compiled binaries (gitignored)
├── keys/ # Private keys (gitignored)
├── tmp/ # Air hot reload temp files (gitignored)
├── Makefile # Build automation
├── .air.toml # Hot reload configuration
└── go.mod # Go module definition
```
## Key Dependencies
- **git.haelnorr.com/h/golib/*** - Custom libraries (env, ezconf, hlog, hws, hwsauth, cookies, jwt)
- **github.com/a-h/templ** - Type-safe HTML templating
- **github.com/uptrace/bun** - PostgreSQL ORM
- **github.com/bwmarrin/discordgo** - Discord API client
- **github.com/pkg/errors** - Error wrapping (use this, not fmt.Errorf)
- **github.com/joho/godotenv** - .env file loading
## Notes for AI Agents
1. **Never commit** .env files, keys/, or generated files (*_templ.go, output.css)
2. **Database operations** should use `bun.Tx` for transaction safety
3. **Templates** are written in templ, not Go html/template - run `templ generate` after changes
4. **Static files** are embedded via `//go:embed` - check pkg/embedfs/
3. **Templates** are written in templ, not Go html/template - run `just templ` after changes
4. **Static files** are embedded via `//go:embed` - check internal/embedfs/
5. **Error messages** should be descriptive and use errors.Wrap for context
6. **Security is critical** - especially in OAuth flows (see pkg/oauth/state_test.go for examples)
7. **Air proxy** runs on port 3000 during development; app runs on 3333
8. **Test coverage** is currently limited - prioritize testing security-critical code
9. **Configuration** uses ezconf pattern - see internal/*/ezconf.go files for examples
10. **Graceful shutdown** is implemented in cmd/oslstats/run.go - follow this pattern
11. When in plan mode, always use the interactive question tool if available
8. **Configuration** uses ezconf pattern - see internal/*/ezconf.go files for examples
9. When in plan mode, always use the interactive question tool if available