Skip to content
8 changes: 5 additions & 3 deletions cmd/fleet-plan/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import (
"github.com/TsekNet/fleet-plan/internal/api"
"github.com/TsekNet/fleet-plan/internal/config"
"github.com/TsekNet/fleet-plan/internal/diff"
"github.com/TsekNet/fleet-plan/internal/merge"
"github.com/TsekNet/fleet-plan/internal/git"
"github.com/TsekNet/fleet-plan/internal/merge"
"github.com/TsekNet/fleet-plan/internal/output"
"github.com/TsekNet/fleet-plan/internal/parser"
"github.com/TsekNet/fleet-plan/internal/teamdir"
)

// Set via -ldflags at build time.
Expand Down Expand Up @@ -132,10 +133,11 @@ func runDiff(cmd *cobra.Command, _ []string) error {
}

if len(repo.Teams) == 0 && len(repo.Errors) == 0 {
dirName := teamdir.Resolve(flagRepo)
if len(teams) > 0 {
return fmt.Errorf("no teams matching %v found in %s/teams/", teams, flagRepo)
return fmt.Errorf("no teams matching %v found in %s/%s/", teams, flagRepo, dirName)
}
return fmt.Errorf("no teams found in %s/teams/\nAre you in a fleet-gitops repo? Try --repo /path/to/repo", flagRepo)
return fmt.Errorf("no teams found in %s/%s/\nAre you in a fleet-gitops repo? Try --repo /path/to/repo", flagRepo, dirName)
}

// Parse baseline (base branch) for subtraction when in --git mode.
Expand Down
9 changes: 6 additions & 3 deletions internal/git/baseline.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"os/exec"
"path/filepath"
"strings"

"github.com/TsekNet/fleet-plan/internal/teamdir"
)

// CheckoutBaseline extracts the base-branch versions of the given files into a
Expand All @@ -26,8 +28,9 @@ func CheckoutBaseline(repoRoot string, baseRef string, files []string) (tmpRoot
}
cleanup = func() { os.RemoveAll(tmpRoot) }

// Ensure teams/ directory exists so ParseRepo doesn't bail early.
os.MkdirAll(filepath.Join(tmpRoot, "teams"), 0o755)
// Ensure the team directory exists so ParseRepo doesn't bail early.
// Mirror whichever name the source repo uses (fleets/ or teams/).
os.MkdirAll(filepath.Join(tmpRoot, teamdir.Resolve(repoRoot)), 0o755)

// Resolve which files we need: the explicitly changed files, plus any
// files they reference (path: directives in team YAML). We start with
Expand Down Expand Up @@ -84,7 +87,7 @@ func collectBaselineFiles(repoRoot, baseRef string, changedFiles []string) []str
// references. Also extract any referenced resource files so the parser can
// resolve them.
for _, f := range changedFiles {
if !strings.HasPrefix(f, "teams/") && f != "base.yml" && f != "default.yml" {
if !teamdir.HasPrefix(f) && f != "base.yml" && f != "default.yml" {
continue
}
content, err := gitShow(repoRoot, baseRef, f)
Expand Down
29 changes: 22 additions & 7 deletions internal/git/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"path/filepath"
"strings"

"github.com/TsekNet/fleet-plan/internal/teamdir"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -40,7 +41,7 @@ func ResolveScope(root string, changedFiles []string, envFile string) Scope {
case f == "base.yml", f == envFile, strings.HasPrefix(f, "labels/") && !strings.HasSuffix(f, ".md"):
scope.IncludeGlobal = true

case strings.HasPrefix(f, "teams/") && (strings.HasSuffix(f, ".yml") || strings.HasSuffix(f, ".yaml")):
case isTeamYAML(f):
name := readTeamName(filepath.Join(root, f))
if name != "" && !teamsSeen[name] {
teamsSeen[name] = true
Expand Down Expand Up @@ -80,7 +81,16 @@ func isFleetResource(f string) bool {
}

func isFleetResourceOrTeam(f string) bool {
return isFleetResource(f) || (strings.HasPrefix(f, "teams/") && (strings.HasSuffix(f, ".yml") || strings.HasSuffix(f, ".yaml")))
return isFleetResource(f) || isTeamYAML(f)
}

// isTeamYAML reports whether a repo-relative path is a per-team YAML file
// under any recognized team directory (fleets/ or teams/).
func isTeamYAML(f string) bool {
if !strings.HasSuffix(f, ".yml") && !strings.HasSuffix(f, ".yaml") {
return false
}
return teamdir.HasPrefix(f)
}

// buildSearchPatterns returns the set of path strings to grep for in team YAMLs.
Expand All @@ -103,12 +113,17 @@ func buildSearchPatterns(root, f string) []string {
return patterns
}

// teamsReferencingAny reads teams/*.yml and returns team names whose file
// content contains any of the given patterns (plain string search).
// teamsReferencingAny reads every team YAML in the repo (fleets/ or teams/)
// and returns team names whose file content contains any of the given patterns
// (plain string search).
func teamsReferencingAny(root string, patterns []string) []string {
ymlMatches, _ := filepath.Glob(filepath.Join(root, "teams", "*.yml"))
yamlMatches, _ := filepath.Glob(filepath.Join(root, "teams", "*.yaml"))
matches := append(ymlMatches, yamlMatches...)
var matches []string
for _, dir := range teamdir.Names() {
for _, ext := range []string{"*.yml", "*.yaml"} {
m, _ := filepath.Glob(filepath.Join(root, dir, ext))
matches = append(matches, m...)
}
}
seen := map[string]bool{}
var names []string
for _, teamFile := range matches {
Expand Down
57 changes: 55 additions & 2 deletions internal/git/scope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ import (
)

// writeTeamFile creates a team YAML file in root/teams/ with the given name
// and optional body content referencing resources.
// and optional body content referencing resources. The legacy teams/ name
// is kept here so existing test cases continue exercising backwards compat.
func writeTeamFile(t *testing.T, root, filename, teamName, body string) {
t.Helper()
dir := filepath.Join(root, "teams")
writeTeamFileIn(t, root, "teams", filename, teamName, body)
}

// writeTeamFileIn is the same as writeTeamFile but lets the caller pick the
// directory name (used to exercise the canonical fleets/ layout).
func writeTeamFileIn(t *testing.T, root, dirName, filename, teamName, body string) {
t.Helper()
dir := filepath.Join(root, dirName)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -130,6 +138,29 @@ func TestResolveScope(t *testing.T) {
wantTeams: []string{"Workstations"},
wantTeamCount: 1,
},
{
name: "fleets/ team YAML change resolves team name",
setup: func(t *testing.T, root string) {
writeTeamFileIn(t, root, "fleets", "infra.yml", "Infrastructure", "")
},
changedFiles: []string{"fleets/infra.yml"},
wantGlobal: false,
wantTeams: []string{"Infrastructure"},
wantChanged: []string{"fleets/infra.yml"},
wantTeamCount: 1,
},
{
name: "resource change finds referencing fleets/ team",
setup: func(t *testing.T, root string) {
writeTeamFileIn(t, root, "fleets", "alpha.yml", "Alpha",
"controls:\n scripts:\n - path: ../scripts/setup.ps1\n")
},
changedFiles: []string{"scripts/setup.ps1"},
wantGlobal: false,
wantTeams: []string{"Alpha"},
wantChanged: []string{"scripts/setup.ps1"},
wantTeamCount: 1,
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -196,6 +227,28 @@ func TestIsFleetResource(t *testing.T) {
}
}

func TestIsTeamYAML(t *testing.T) {
t.Parallel()
cases := []struct {
path string
want bool
}{
{"fleets/workstations.yml", true},
{"teams/workstations.yml", true},
{"fleets/infra.yaml", true},
{"teams/infra.yaml", true},
{"fleets/workstations.md", false},
{"policies/disk.yml", false},
{"fleets-extra/x.yml", false},
{"", false},
}
for _, c := range cases {
if got := isTeamYAML(c.path); got != c.want {
t.Errorf("isTeamYAML(%q) = %v, want %v", c.path, got, c.want)
}
}
}

func contains(ss []string, s string) bool {
for _, v := range ss {
if v == s {
Expand Down
Loading