Files
cad-atlas/internal/atlas/workflow.go
T
mathiasandClaude Opus 4.8 a9c72d6ca8
CD / Detect unsubstituted template (push) Successful in 0s
CD / Lint / Test / Vet (push) Successful in 5s
CD / Build & Import (push) Successful in 14s
CD / Deploy via GitOps (push) Successful in 1s
feat(atlas): Phase B — data-driven /api/atlas.json, CI stage generated from cd.yml
The atlas no longer hand-maintains its content. Authored data lives in one place
(internal/atlas/atlas.json); the Go layer overlays sourced facts and serves the
result at /api/atlas.json; the frontend fetches + renders (inline arrays kept only
as an offline fallback). First generated source: the CI/CD stage's nodes are parsed
from the repo's own .gitea/workflows/cd.yml — so the viz shows the pipeline that
actually runs (guard/check/build/deploy), dropping the aspirational var-go/oath-gate
node that isn't wired yet. That's the point: it can't drift from the real pipeline.

New internal/atlas package (JobsFromWorkflow, Build) built test-first. Adds
gopkg.in/yaml.v3 (justified: parsing the workflow YAML; stdlib has no YAML).

Verified: go build/vet/lint(0)/test green; /api/atlas.json → 9 stages, CI = real
jobs; frontend renders from the fetch (screenshot).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 00:34:06 +02:00

49 lines
1.6 KiB
Go

// Package atlas builds the CAD Atlas data model, deriving parts from real
// sources (the repo's own CI workflow, infra manifests, the live cluster)
// so the visualization can't drift from reality.
package atlas
import (
"fmt"
"gopkg.in/yaml.v3"
)
// JobsFromWorkflow extracts the job names, in document order, from a Gitea
// Actions / GitHub Actions workflow YAML. Used to generate the CI/CD stage of
// the atlas from the pipeline that actually runs, rather than hand-authoring it.
func JobsFromWorkflow(workflow []byte) ([]string, error) {
var doc yaml.Node
if err := yaml.Unmarshal(workflow, &doc); err != nil {
return nil, fmt.Errorf("parse workflow: %w", err)
}
if len(doc.Content) == 0 || doc.Content[0].Kind != yaml.MappingNode {
return nil, fmt.Errorf("workflow: expected a top-level mapping")
}
jobs := mappingValue(doc.Content[0], "jobs")
if jobs == nil {
return nil, fmt.Errorf("workflow: no jobs block")
}
if jobs.Kind != yaml.MappingNode {
return nil, fmt.Errorf("workflow: jobs is not a mapping")
}
// A mapping node stores keys and values as alternating Content entries;
// keys are the even indices, in document order.
names := make([]string, 0, len(jobs.Content)/2)
for i := 0; i+1 < len(jobs.Content); i += 2 {
names = append(names, jobs.Content[i].Value)
}
return names, nil
}
// mappingValue returns the value node for key in a YAML mapping node, or nil.
func mappingValue(m *yaml.Node, key string) *yaml.Node {
for i := 0; i+1 < len(m.Content); i += 2 {
if m.Content[i].Value == key {
return m.Content[i+1]
}
}
return nil
}