rdev/internal/adapter/postgres/build_events.go
jordan c59d348040 chore: prepare for composable monorepo template implementation
This commit captures the current state before implementing the composable
monorepo template system. Key changes included:

Infrastructure:
- Add CockroachDB provisioner adapter for database provisioning
- Add Redis provisioner adapter for cache provisioning
- Add build events system with PostgreSQL storage
- Add WebSocket endpoint for real-time build progress

Code agent improvements:
- Fix Claude Code adapter to use default allowed tools instead of dangerously-skip-permissions
- Add context-aware stream closing for cancellation support
- Improve parser tests for edge cases

Build system:
- Add build event constants and metrics
- Remove deprecated git_operations.go (replaced by pod_git_operations.go)
- Add rollback logic for multi-step provisioning operations

Documentation:
- Add composable-monorepo feature documentation
- Add DNS/Cloudflare service documentation
- Update deployment and troubleshooting guides

Cookbooks:
- Add fullstack-app cookbook
- Refactor landing-test with shared library

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:39:28 -07:00

108 lines
3.0 KiB
Go

package postgres
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/port"
)
// BuildEventRepository implements port.BuildEventStore using PostgreSQL.
type BuildEventRepository struct {
db *sql.DB
}
// NewBuildEventRepository creates a new build event repository.
func NewBuildEventRepository(db *sql.DB) *BuildEventRepository {
return &BuildEventRepository{db: db}
}
// Ensure BuildEventRepository implements port.BuildEventStore at compile time.
var _ port.BuildEventStore = (*BuildEventRepository)(nil)
// buildEventRow is the database representation of a build event.
type buildEventRow struct {
ID string `db:"id"`
TaskID string `db:"task_id"`
ProjectID string `db:"project_id"`
Type string `db:"type"`
Sequence int64 `db:"sequence"`
Timestamp time.Time `db:"timestamp"`
Data []byte `db:"data"`
CreatedAt time.Time `db:"created_at"`
}
// Record stores a build event for later replay.
func (r *BuildEventRepository) Record(ctx context.Context, event *domain.BuildEvent) error {
dataBytes, err := json.Marshal(event.Data)
if err != nil {
return err
}
_, err = r.db.ExecContext(ctx, `
INSERT INTO build_events (id, task_id, project_id, type, sequence, timestamp, data)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO NOTHING
`, event.ID, event.TaskID, event.ProjectID, event.Type, event.Sequence, event.Timestamp, dataBytes)
return err
}
// ListByTask retrieves events for a task, optionally after a sequence number.
func (r *BuildEventRepository) ListByTask(ctx context.Context, taskID string, afterSequence int64) ([]*domain.BuildEvent, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, task_id, project_id, type, sequence, timestamp, data
FROM build_events
WHERE task_id = $1 AND sequence > $2
ORDER BY sequence ASC
`, taskID, afterSequence)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
var events []*domain.BuildEvent
for rows.Next() {
var row buildEventRow
if err := rows.Scan(&row.ID, &row.TaskID, &row.ProjectID, &row.Type, &row.Sequence, &row.Timestamp, &row.Data); err != nil {
return nil, err
}
var data domain.BuildEventData
if err := json.Unmarshal(row.Data, &data); err != nil {
// If unmarshal fails, use empty data
data = domain.BuildEventData{}
}
events = append(events, &domain.BuildEvent{
ID: row.ID,
TaskID: row.TaskID,
ProjectID: row.ProjectID,
Type: domain.BuildEventType(row.Type),
Sequence: row.Sequence,
Timestamp: row.Timestamp,
Data: data,
})
}
return events, rows.Err()
}
// Cleanup removes events older than the specified age.
func (r *BuildEventRepository) Cleanup(ctx context.Context, olderThan time.Duration) (int64, error) {
cutoff := time.Now().Add(-olderThan)
result, err := r.db.ExecContext(ctx, `
DELETE FROM build_events
WHERE created_at < $1
`, cutoff)
if err != nil {
return 0, err
}
return result.RowsAffected()
}