rdev/internal/sdlc/branch.go
jordan f22b220c6d feat: add SDLC branch management, merge, archive, and orchestrator APIs
Add branch lifecycle commands (branch, merge, archive) to the SDLC CLI.
Introduce orchestrator handler and service for multi-step SDLC workflows.
Expand skeleton template with 15 Claude commands covering the full feature
lifecycle. Extend classifier rules, error types, and executor port for
branch operations. Split rules.go and classifier_test.go to stay within
500-line limit.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:30:03 -07:00

138 lines
3.7 KiB
Go

package sdlc
import (
"fmt"
"os"
"path/filepath"
"time"
"gopkg.in/yaml.v3"
)
// BranchManifest tracks branch metadata for a feature.
type BranchManifest struct {
Name string `yaml:"name" json:"name"`
Feature string `yaml:"feature" json:"feature"`
BaseBranch string `yaml:"base_branch" json:"base_branch"`
CreatedAt time.Time `yaml:"created_at" json:"created_at"`
LastSyncAt *time.Time `yaml:"last_sync_at,omitempty" json:"last_sync_at,omitempty"`
MergedAt *time.Time `yaml:"merged_at,omitempty" json:"merged_at,omitempty"`
MergeStrategy string `yaml:"merge_strategy,omitempty" json:"merge_strategy,omitempty"`
}
// BranchPath returns the path to a branch manifest file.
func BranchPath(root, branchName string) string {
return filepath.Join(root, SDLCDir, BranchesDir, branchName+".yaml")
}
// CreateBranch creates a new branch manifest for a feature.
// It validates the feature exists and constructs the branch name from config.
func CreateBranch(root, slug string, cfg *Config) (*BranchManifest, error) {
f, err := LoadFeature(root, slug)
if err != nil {
return nil, err
}
branchName := cfg.Branches.FeaturePrefix + slug
// Check if branch manifest already exists
path := BranchPath(root, branchName)
if _, err := os.Stat(path); err == nil {
return nil, ErrBranchExists
}
manifest := &BranchManifest{
Name: branchName,
Feature: slug,
BaseBranch: cfg.Branches.Main,
CreatedAt: time.Now().UTC(),
}
if err := SaveBranch(root, manifest); err != nil {
return nil, err
}
// Update feature with branch reference
f.Branch = branchName
if err := f.Save(root); err != nil {
return nil, err
}
return manifest, nil
}
// LoadBranch reads a branch manifest from disk.
func LoadBranch(root, branchName string) (*BranchManifest, error) {
path := BranchPath(root, branchName)
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrBranchNotFound
}
return nil, fmt.Errorf("read branch manifest: %w", err)
}
var m BranchManifest
if err := yaml.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("parse branch manifest: %w", err)
}
return &m, nil
}
// SaveBranch writes a branch manifest to disk.
func SaveBranch(root string, manifest *BranchManifest) error {
path := BranchPath(root, manifest.Name)
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("create branch directory: %w", err)
}
data, err := yaml.Marshal(manifest)
if err != nil {
return fmt.Errorf("marshal branch manifest: %w", err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("write branch manifest: %w", err)
}
return nil
}
// MergeChecklist returns a list of unmet gates that block merging.
// An empty list means the feature is ready to merge.
func MergeChecklist(root, slug string) ([]string, error) {
f, err := LoadFeature(root, slug)
if err != nil {
return nil, err
}
var unmet []string
// Must be in merge phase
if f.Phase != PhaseMerge {
unmet = append(unmet, fmt.Sprintf("feature is in phase %s, must be in merge", f.Phase))
}
// Review must be passed
if art := f.GetArtifact(ArtifactReview); art == nil || art.Status != StatusPassed {
unmet = append(unmet, "code review not passed")
}
// Audit must be passed
if art := f.GetArtifact(ArtifactAudit); art == nil || art.Status != StatusPassed {
unmet = append(unmet, "security audit not passed")
}
// QA must be passed
if art := f.GetArtifact(ArtifactQAResults); art == nil || art.Status != StatusPassed {
unmet = append(unmet, "QA tests not passed")
}
// No blockers
if f.IsBlocked() {
unmet = append(unmet, fmt.Sprintf("feature has %d blocker(s)", len(f.Blockers)))
}
return unmet, nil
}