Git Repository
Stackyard
Stackyard scannt lokale Entwicklungsordner und bündelt Projekte, Git-Status, TODOs, Toolchains, Docker-Compose-Setups, Datei- und Launcher-Funktionen sowie Operations-Informationen in einer lokalen WebUI.
HTTPS
https://zanvex.de/git/stackyard.gitinternal/operations/operations.go
Zum Verzeichnispackage operations
import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v3"
"stackyard/internal/project"
"stackyard/internal/todo"
)
type Manifest struct {
Description string `yaml:"description"`
Provider string `yaml:"provider"`
Dependencies []string `yaml:"dependencies"`
Actions []Action `yaml:"actions"`
Services []ServiceProfile `yaml:"services"`
}
type Action struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
}
type ServiceProfile struct {
Name string `yaml:"name"`
Port int `yaml:"port"`
URL string `yaml:"url"`
HealthPath string `yaml:"health_path"`
}
type PortStatus struct {
ProjectID string
ProjectName string
Name string
Port int
URL string
Reachable bool
Healthy bool
Detail string
}
type TodoItem struct {
ProjectID string
ProjectName string
Path string
Line int
Text string
Done bool
}
var allowedActions = map[string][]string{
"go-test": {"go", "test", "./..."},
"go-build": {"go", "build", "./..."},
"go-run": {"go", "run", "."},
}
func IsLongRunningAction(kind string) bool {
return kind == "go-run"
}
func LoadManifest(projectDir string) Manifest {
data, err := os.ReadFile(filepath.Join(projectDir, ".stackyard", "project.yaml"))
if err != nil {
return Manifest{}
}
var manifest Manifest
if yaml.Unmarshal(data, &manifest) != nil {
return Manifest{}
}
return manifest
}
func ValidateAction(kind string) error {
if _, ok := allowedActions[kind]; !ok {
return fmt.Errorf("unsupported action kind %q", kind)
}
return nil
}
func RunAction(ctx context.Context, projectDir, kind string) (string, error) {
args, ok := allowedActions[kind]
if !ok {
return "", fmt.Errorf("unsupported action kind %q", kind)
}
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
cmd.Dir = projectDir
out, err := cmd.CombinedOutput()
if len(out) > 64<<10 {
out = out[len(out)-(64<<10):]
}
if err != nil {
return string(out), fmt.Errorf("%s: %w", kind, err)
}
return string(out), nil
}
func CollectPorts(ctx context.Context, projects []project.Project) []PortStatus {
var values []PortStatus
client := &http.Client{Timeout: 800 * time.Millisecond}
for _, current := range projects {
manifest := LoadManifest(current.Path)
profiles := manifest.Services
if len(profiles) == 0 {
for _, port := range current.Ports {
profiles = append(profiles, ServiceProfile{Name: "Detected", Port: port, URL: "http://127.0.0.1:" + strconv.Itoa(port)})
}
}
for _, service := range profiles {
if service.Port <= 0 || service.Port > 65535 {
continue
}
value := PortStatus{ProjectID: current.ID, ProjectName: current.Name, Name: service.Name, Port: service.Port, URL: service.URL}
conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(service.Port)), 250*time.Millisecond)
if err == nil {
value.Reachable = true
_ = conn.Close()
}
if value.Reachable && service.URL != "" && isLoopbackURL(service.URL) {
healthURL := strings.TrimRight(service.URL, "/") + service.HealthPath
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
if response, requestErr := client.Do(req); requestErr == nil {
value.Healthy = response.StatusCode >= 200 && response.StatusCode < 400
value.Detail = response.Status
_ = response.Body.Close()
}
} else if service.URL != "" && !isLoopbackURL(service.URL) {
value.Detail = "Externe URL blockiert"
value.URL = ""
}
values = append(values, value)
}
}
return values
}
func isLoopbackURL(raw string) bool {
parsed, err := url.Parse(raw)
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return false
}
host := strings.ToLower(parsed.Hostname())
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}
func CollectTodos(projects []project.Project) []TodoItem {
var values []TodoItem
for _, current := range projects {
path, body, err := todo.Read(current.Path)
if err != nil || body == "" {
continue
}
scanner := bufio.NewScanner(strings.NewReader(body))
line := 0
for scanner.Scan() {
line++
text := strings.TrimSpace(scanner.Text())
done := strings.HasPrefix(strings.ToLower(text), "- [x]")
if !done && !strings.HasPrefix(text, "- [ ]") {
continue
}
values = append(values, TodoItem{ProjectID: current.ID, ProjectName: current.Name, Path: path, Line: line, Text: strings.TrimSpace(text[5:]), Done: done})
}
}
return values
}