Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@

dist/
gh_foundations
github-foundations-cli
7 changes: 7 additions & 0 deletions README.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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