package admin import ( "bytes" "context" "fmt" "os/exec" "sync" "time" ) type Builder struct { store *Store exporter *Exporter siteDir string queue chan string once sync.Once } func NewBuilder(store *Store, exporter *Exporter, siteDir string) *Builder { return &Builder{ store: store, exporter: exporter, siteDir: siteDir, queue: make(chan string, 32), } } func (b *Builder) Start(ctx context.Context) { b.once.Do(func() { go b.loop(ctx) }) } func (b *Builder) Enqueue(jobID string) bool { select { case b.queue <- jobID: return true default: return false } } func (b *Builder) loop(ctx context.Context) { for { select { case <-ctx.Done(): return case jobID := <-b.queue: b.runBuildJob(ctx, jobID) } } } func (b *Builder) runBuildJob(ctx context.Context, jobID string) { log, err := b.run(ctx, jobID) if err != nil { _ = b.store.MarkBuildJobFailed(context.Background(), jobID, log, err.Error()) return } _ = b.store.MarkBuildJobSuccess(context.Background(), jobID, log) } func (b *Builder) run(ctx context.Context, jobID string) (string, error) { if err := b.store.MarkBuildJobRunning(ctx, jobID); err != nil { return "", err } posts, err := b.store.PublishedPostsForExport(ctx) if err != nil { return "", err } if err := b.exporter.ExportPublishedPosts(ctx, posts); err != nil { return "", err } buildCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) defer cancel() cmd := exec.CommandContext(buildCtx, "npm", "run", "build") cmd.Dir = b.siteDir var output bytes.Buffer cmd.Stdout = &output cmd.Stderr = &output if err := cmd.Run(); err != nil { return output.String(), fmt.Errorf("astro build failed: %w", err) } return output.String(), nil }