Skip to content
Open
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@

dist/
gh_foundations
github-foundations-cli
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ Where `<resource>` is one of the following:
- repos:
- `--ghas`, `-g` List repositories with GHAS enabled.


Notes:

- The `repos` command parses `repositories/terragrunt.hcl` files. It now supports Terragrunt `locals {}` blocks and replacements of `local.<key>` references inside the `inputs` object. If parsing with Viper fails, the tool falls back to native HCL parsing.
- Logging during `list repos` is suppressed to ensure clean machine-readable output (e.g. `['org/repo1', 'org/repo2']` or `[]`).


### Help

Display help for the tool.
Expand Down
7 changes: 6 additions & 1 deletion cmd/gen/repository_set/repository_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ var GenRepositorySetCmd = &cobra.Command{
zone.NewGlobal()
var repositorySet *githubfoundations.RepositorySetInput
if terraformerStateFile != "" {
repositorySet = genFromTerraformerFile(terraformerStateFile)
var err error
repositorySet, err = genFromTerraformerFile(terraformerStateFile)
if err != nil {
fmt.Println("Error reading terraformer state file:", err)
os.Exit(1)
}
} else {
var err error
repositorySet, err = runInteractive()
Expand Down
26 changes: 13 additions & 13 deletions cmd/gen/repository_set/terraformerState.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package repositoryset

import (
"gh_foundations/internal/pkg/functions"
"log"
"os"

"github.com/tidwall/gjson"

githubfoundations "gh_foundations/internal/pkg/types/github_foundations"
"os"
"log"
"gh_foundations/internal/pkg/functions"
githubfoundations "gh_foundations/internal/pkg/types/github_foundations"
"github.com/tidwall/gjson"
)

func genFromTerraformerFile(stateFile string) *githubfoundations.RepositorySetInput {
stateBytes, err := os.ReadFile(stateFile)
if err != nil {
log.Fatalf("Error reading state file %s. %s", stateFile, err.Error())
}

func genFromTerraformerFile(stateFile string) (*githubfoundations.RepositorySetInput, error) {
stateBytes, err := os.ReadFile(stateFile)
if err != nil {
log.Printf("terraformerState: error reading state file %s: %v", stateFile, err)
return nil, err
}
result := gjson.Parse(string(stateBytes))

list := result.Get("modules.0.resources").Map()
Expand Down Expand Up @@ -51,5 +51,5 @@ func genFromTerraformerFile(stateFile string) *githubfoundations.RepositorySetIn
repository.UserPermissions = repositoryUserPermissions[repository.Name]
}

return repositorySets
return repositorySets, nil
}
4 changes: 2 additions & 2 deletions cmd/list/orgs/orgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import (
var OrgsCmd = &cobra.Command{
Use: "orgs",
Short: "List managed organizations's slugs.",
Long: `This command reads the "providers.hcl" files in the "providers" directory and lists the organization slugs that are managed by the tool.`,
Long: `This command reads the "providers.hcl" or "root.hcl" files in the organizations directory and lists the organization slugs that are managed by the tool.`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires the path of the \"providers\" directory")
return errors.New("requires the path of the organizations directory")
}
return nil
},
Expand Down
31 changes: 25 additions & 6 deletions cmd/list/repos/repos.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ Copyright © 2024 NAME HERE <EMAIL ADDRESS>
package list

import (
"encoding/json"
"errors"
"fmt"
"gh_foundations/internal/pkg/functions"
"gh_foundations/internal/pkg/types/status"
"io"
"log"
"sort"
"strings"

"github.com/spf13/cobra"
)

var ghas bool
var verbose bool
var outputFormat string

var ReposCmd = &cobra.Command{
Use: "repos",
Expand All @@ -30,9 +35,15 @@ var ReposCmd = &cobra.Command{

reposDir := args[0]

// Suppress all logs unless verbose requested
if !verbose {
log.SetOutput(io.Discard)
}
orgSet, err := functions.FindManagedRepos(reposDir)
if err != nil {
log.Fatalf("Error in findManagedRepos: %s", err)
log.Printf("list repos: error discovering repositories: %v", err)
fmt.Println("[]")
return
}

repoList := flattenRepos(orgSet)
Expand All @@ -43,17 +54,25 @@ var ReposCmd = &cobra.Command{
log.Printf("Found %d repositories with GHAS enabled\n", len(repoList))
}

repos := "[]"
if len(repoList) > 0 {
repos = fmt.Sprintf("['%s']", strings.Join(repoList, "', '"))
sort.Strings(repoList)
if outputFormat == "json" {
data, _ := json.Marshal(repoList) // data is always slice of strings, marshal can't fail
fmt.Println(string(data))
return
}

fmt.Println(repos)
// Default legacy format
if len(repoList) == 0 {
fmt.Println("[]")
return
}
fmt.Printf("['%s']\n", strings.Join(repoList, "', '"))
},
}

func init() {
ReposCmd.Flags().BoolVarP(&ghas, "ghas", "g", false, "List repositories with GHAS enabled")
ReposCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show diagnostic logs while listing")
ReposCmd.Flags().StringVarP(&outputFormat, "format", "f", "legacy", "Output format: legacy | json")
}

// Return only the names of the repositories managed by the tool
Expand Down
65 changes: 32 additions & 33 deletions internal/pkg/functions/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ func findOrgsFromFilenames(hclFiles []string) map[string][]string {
// List all of the organizations managed by the tool's slugs
func FindManagedOrgSlugs(orgsDir string) ([]string, error) {

orgFiles, err := findConfigFiles(orgsDir, "providers.hcl")
if err != nil {
log.Fatalf("Error in findOrgFiles: %s", err)
// Look for both providers.hcl and root.hcl files for backward compatibility
// and to support the new Terragrunt naming convention
orgFiles, err := findConfigFiles(orgsDir, "providers.hcl", "root.hcl")
if err != nil {
log.Printf("FindManagedOrgSlugs: error finding org files: %v", err)
return make([]string, 0), err
}
}

// Walk the orgFiles and get all the providers.hcl files
// Walk the orgFiles and get all the providers.hcl and root.hcl files
var orgs []string
for _, file := range orgFiles {
log.Printf("Working on file: %s\n", file)
Expand All @@ -55,28 +57,27 @@ func FindManagedOrgSlugs(orgsDir string) ([]string, error) {

// List all of the relevant configs managed by the tool
// The first parameter is the root directory to search in
// The second parameter is the file name pattern to match
// The second parameter is the file name pattern(s) to match
func findConfigFiles(rootDir string, fileNamePattern ...string) ([]string, error) {

// There should be 1 or 0 file name patterns to match
patternString := ""
if len(fileNamePattern) == 1 {
patternString = fileNamePattern[0]
// Default to "repositories/terragrunt.hcl" if no patterns provided
patterns := fileNamePattern
if len(patterns) == 0 {
patterns = []string{"repositories/terragrunt.hcl"}
}


var hclFiles []string
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// Find files that match the fileNamePattern. Default to "repositories/terragrunt.hcl"
if patternString == "" {
patternString = "repositories/terragrunt.hcl"
}
if strings.HasSuffix(path, patternString) {
hclFiles = append(hclFiles, path)
// Check if the path matches any of the patterns
for _, pattern := range patterns {
if strings.HasSuffix(path, pattern) {
hclFiles = append(hclFiles, path)
break // Only add each file once
}
}

return nil
Expand All @@ -91,21 +92,19 @@ func findConfigFiles(rootDir string, fileNamePattern ...string) ([]string, error

// List all of the repositories managed by the tool
func FindManagedRepos(reposDir string) (status.OrgSet, error) {
files, err := findConfigFiles(reposDir)

orgFiles := findOrgsFromFilenames(files)

if err != nil {
log.Fatalf("Error in findOrgFiles: %s", err)
files, err := findConfigFiles(reposDir)
if err != nil {
log.Printf("FindManagedRepos: error finding repo files: %v", err)
return status.OrgSet{}, err
}
}
orgFiles := findOrgsFromFilenames(files)

// Get the absolute path of the root directory
absRootPath, err := filepath.Abs(reposDir)
if err != nil {
log.Fatalf("Error in filepath.Abs: %s", err)
absRootPath, err := filepath.Abs(reposDir)
if err != nil {
log.Printf("FindManagedRepos: filepath.Abs error: %v", err)
return status.OrgSet{}, err
}
}

var orgSet status.OrgSet
orgSet.OrgProjectSets = make(map[string]status.OrgProjectSet)
Expand Down Expand Up @@ -137,11 +136,11 @@ func FindManagedRepos(reposDir string) (status.OrgSet, error) {
Path: file,
}

inputs, err := hclFile.GetInputsFromFile()
if err != nil {
log.Fatalf(`Error in getInputsFromFile: %s`, err)
return orgSet, err
}
inputs, err := hclFile.GetInputsFromFile()
if err != nil {
log.Printf("FindManagedRepos: skipping file %s due to parse error: %v", file, err)
continue
}

log.Printf("Repository Set has %d private repositories and %d public repositories", len(inputs.PrivateRepositories), len(inputs.PublicRepositories))
var repoSet githubfoundations.RepositorySetInput
Expand Down
Loading