Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Add --connect-timeout 10 and --max-time 15 to all verify step curl calls to prevent hanging on registry health checks - Fix cli template: depends_on [deps] -> [preflight] for consistency - Add cross-reference comment to service template about verify logic being replicated across all 5 component templates - Document component CI step rules in composable-monorepo.md - Compile regexes at package level instead of per-call in component_updates.go - Add component_updates_test.go Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
244 lines
7.0 KiB
Go
244 lines
7.0 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/logging"
|
|
)
|
|
|
|
var (
|
|
reDeployStep = regexp.MustCompile(`(?m)^deploy-([a-zA-Z0-9_-]+):`)
|
|
reDependsOn = regexp.MustCompile(`depends_on:\s*\[([^\]]*)\]`)
|
|
)
|
|
|
|
// updateProcfile adds an entry for the component.
|
|
func (s *ComponentService) updateProcfile(existing string, componentType domain.ComponentType, componentName, componentPath string, _ int) string {
|
|
var entry string
|
|
|
|
switch componentType {
|
|
case domain.ComponentTypeService:
|
|
entry = fmt.Sprintf("%s: cd %s && make run", componentName, componentPath)
|
|
case domain.ComponentTypeWorker:
|
|
entry = fmt.Sprintf("%s: cd %s && make run", componentName, componentPath)
|
|
case domain.ComponentTypeAppAstro, domain.ComponentTypeAppReact:
|
|
entry = fmt.Sprintf("%s: cd %s && npm run dev", componentName, componentPath)
|
|
case domain.ComponentTypeCLI:
|
|
// CLIs don't run as processes
|
|
return existing
|
|
}
|
|
|
|
// Add the entry before any empty line at the end, or at the end
|
|
lines := strings.Split(strings.TrimRight(existing, "\n"), "\n")
|
|
lines = append(lines, entry)
|
|
return strings.Join(lines, "\n") + "\n"
|
|
}
|
|
|
|
// updateGoWork adds a use directive for Go components.
|
|
func (s *ComponentService) updateGoWork(existing, componentPath string) string {
|
|
useLine := fmt.Sprintf("use ./%s", componentPath)
|
|
|
|
// Check if already present
|
|
if strings.Contains(existing, useLine) {
|
|
return existing
|
|
}
|
|
|
|
// Find where to insert: after the last 'use' line or after 'go X.XX'
|
|
lines := strings.Split(existing, "\n")
|
|
insertIdx := len(lines) - 1
|
|
|
|
// Find the last use statement
|
|
for i := len(lines) - 1; i >= 0; i-- {
|
|
trimmed := strings.TrimSpace(lines[i])
|
|
if strings.HasPrefix(trimmed, "use ") {
|
|
insertIdx = i + 1
|
|
break
|
|
}
|
|
if strings.HasPrefix(trimmed, "go ") {
|
|
insertIdx = i + 1
|
|
}
|
|
}
|
|
|
|
// Insert the new use line
|
|
newLines := make([]string, 0, len(lines)+1)
|
|
newLines = append(newLines, lines[:insertIdx]...)
|
|
newLines = append(newLines, useLine)
|
|
newLines = append(newLines, lines[insertIdx:]...)
|
|
|
|
return strings.Join(newLines, "\n")
|
|
}
|
|
|
|
// updateWoodpeckerYml inserts the component step at the COMPONENT_STEPS_BELOW marker
|
|
// and updates build-complete's depends_on to include the new deploy step.
|
|
func (s *ComponentService) updateWoodpeckerYml(existing, stepYaml string) string {
|
|
marker := "# COMPONENT_STEPS_BELOW"
|
|
|
|
if !strings.Contains(existing, marker) {
|
|
log := logging.Default().WithService("component")
|
|
log.Warn("COMPONENT_STEPS_BELOW marker not found in .woodpecker.yml")
|
|
return existing
|
|
}
|
|
|
|
// Indent the step YAML properly (2 spaces for YAML steps)
|
|
var sb strings.Builder
|
|
lines := strings.Split(strings.TrimSpace(stepYaml), "\n")
|
|
for _, line := range lines {
|
|
sb.WriteString(" ")
|
|
sb.WriteString(line)
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Insert after the marker
|
|
result := strings.Replace(existing, marker, marker+"\n\n"+strings.TrimRight(sb.String(), "\n"), 1)
|
|
|
|
// Extract deploy step name from the step YAML (e.g., "deploy-studio-api:")
|
|
deployStep := extractDeployStepName(stepYaml)
|
|
if deployStep != "" {
|
|
result = addBuildCompleteDep(result, deployStep)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// extractDeployStepName finds the deploy-{name}: key in a step YAML block.
|
|
func extractDeployStepName(stepYaml string) string {
|
|
match := reDeployStep.FindStringSubmatch(stepYaml)
|
|
if len(match) >= 2 {
|
|
return "deploy-" + match[1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// addBuildCompleteDep appends a step name to build-complete's depends_on line
|
|
// identified by the BUILD_COMPLETE_DEPS marker.
|
|
func addBuildCompleteDep(yml, stepName string) string {
|
|
const depsMarker = "# BUILD_COMPLETE_DEPS"
|
|
|
|
lines := strings.Split(yml, "\n")
|
|
for i, line := range lines {
|
|
if !strings.Contains(line, depsMarker) {
|
|
continue
|
|
}
|
|
|
|
// Parse existing depends_on array from the line
|
|
// Format: " depends_on: [preflight, deploy-foo] # BUILD_COMPLETE_DEPS"
|
|
match := reDependsOn.FindStringSubmatch(line)
|
|
if len(match) < 2 {
|
|
break
|
|
}
|
|
|
|
// Parse existing deps
|
|
existing := strings.TrimSpace(match[1])
|
|
var deps []string
|
|
for _, d := range strings.Split(existing, ",") {
|
|
d = strings.TrimSpace(d)
|
|
if d != "" {
|
|
deps = append(deps, d)
|
|
}
|
|
}
|
|
|
|
// Append new dep if not already present
|
|
for _, d := range deps {
|
|
if d == stepName {
|
|
return yml
|
|
}
|
|
}
|
|
deps = append(deps, stepName)
|
|
|
|
// Reconstruct the line preserving indentation
|
|
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
|
newLine := fmt.Sprintf("%sdepends_on: [%s] %s", indent, strings.Join(deps, ", "), depsMarker)
|
|
lines[i] = newLine
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
return yml
|
|
}
|
|
|
|
// updateClaudeMd adds the component to the routing table.
|
|
func (s *ComponentService) updateClaudeMd(existing string, componentType domain.ComponentType, componentName, componentPath string) string {
|
|
// Find the "## Components" section and add entry
|
|
marker := "<!-- Components will be listed here as they're added -->"
|
|
|
|
var description string
|
|
switch componentType {
|
|
case domain.ComponentTypeService:
|
|
description = "API service"
|
|
case domain.ComponentTypeWorker:
|
|
description = "Background worker"
|
|
case domain.ComponentTypeAppAstro:
|
|
description = "Astro app"
|
|
case domain.ComponentTypeAppReact:
|
|
description = "React app"
|
|
case domain.ComponentTypeCLI:
|
|
description = "CLI tool"
|
|
}
|
|
|
|
entry := fmt.Sprintf("| **%s** | %s | `%s/` |", componentName, description, componentPath)
|
|
|
|
if strings.Contains(existing, marker) {
|
|
// First component - replace the marker with a table
|
|
table := fmt.Sprintf(`| Component | Type | Path |
|
|
|-----------|------|------|
|
|
%s
|
|
`, entry)
|
|
return strings.Replace(existing, marker, table, 1)
|
|
}
|
|
|
|
// Add to existing table - find the last table row in ## Components section
|
|
lines := strings.Split(existing, "\n")
|
|
inComponents := false
|
|
insertIdx := -1
|
|
|
|
for i, line := range lines {
|
|
if strings.HasPrefix(line, "## Components") {
|
|
inComponents = true
|
|
continue
|
|
}
|
|
if inComponents && strings.HasPrefix(line, "## ") {
|
|
// End of Components section
|
|
insertIdx = i
|
|
break
|
|
}
|
|
if inComponents && strings.HasPrefix(line, "|") {
|
|
insertIdx = i + 1
|
|
}
|
|
}
|
|
|
|
if insertIdx > 0 {
|
|
// Insert the new entry
|
|
newLines := make([]string, 0, len(lines)+1)
|
|
newLines = append(newLines, lines[:insertIdx]...)
|
|
newLines = append(newLines, entry)
|
|
newLines = append(newLines, lines[insertIdx:]...)
|
|
return strings.Join(newLines, "\n")
|
|
}
|
|
|
|
return existing
|
|
}
|
|
|
|
// removeProcfileEntry removes a component entry from the Procfile.
|
|
func (s *ComponentService) removeProcfileEntry(procfile, componentName string) string {
|
|
var lines []string
|
|
for _, line := range strings.Split(procfile, "\n") {
|
|
if !strings.HasPrefix(strings.TrimSpace(line), componentName+":") {
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
// removeGoWorkEntry removes a use directive from go.work.
|
|
func (s *ComponentService) removeGoWorkEntry(goWork, componentPath string) string {
|
|
useLine := "use ./" + componentPath
|
|
var lines []string
|
|
for _, line := range strings.Split(goWork, "\n") {
|
|
if strings.TrimSpace(line) != useLine {
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|