diff --git a/feature/github-repo-importer/Justfile b/feature/github-repo-importer/Justfile index e007289..fd2313b 100644 --- a/feature/github-repo-importer/Justfile +++ b/feature/github-repo-importer/Justfile @@ -1,5 +1,6 @@ import-repo repoName: #!/usr/bin/env bash + set -euo pipefail go run main.go import {{repoName}} IFS='/' read -r owner repo <<< {{repoName}} mkdir -p "../../feature/github-repo-provisioning/gcss_config/importer_tmp_dir/" diff --git a/feature/github-repo-importer/cmd/import.go b/feature/github-repo-importer/cmd/import.go index c1844f9..f25e0c0 100644 --- a/feature/github-repo-importer/cmd/import.go +++ b/feature/github-repo-importer/cmd/import.go @@ -20,7 +20,7 @@ var importCmd = &cobra.Command{ repo, err := github.ImportRepo(repository) if err != nil { - return fmt.Errorf("failed to import repo: %w", err) + return err } if err := github.WriteRepositoryToYaml(repo); err != nil { diff --git a/feature/github-repo-importer/cmd/root.go b/feature/github-repo-importer/cmd/root.go index 4dfc485..211a274 100644 --- a/feature/github-repo-importer/cmd/root.go +++ b/feature/github-repo-importer/cmd/root.go @@ -1,19 +1,33 @@ package cmd import ( + "fmt" "os" + "strings" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ - Use: "importer", - Short: "A CLI tool to fetch GitHub repository details, branch protection rules & rulesets", + Use: "importer", + Short: "A CLI tool to fetch GitHub repository details, branch protection rules & rulesets", + SilenceUsage: true, + SilenceErrors: true, } func Execute() { err := rootCmd.Execute() if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Error:", err) + if os.Getenv("GITHUB_ACTIONS") == "true" { + _, _ = fmt.Fprintf(os.Stdout, "::error::%s\n", escapeAnnotation(err.Error())) + } os.Exit(1) } } + +// escapeAnnotation escapes a message for use in a GitHub Actions workflow command. +func escapeAnnotation(s string) string { + r := strings.NewReplacer("%", "%25", "\r", "%0D", "\n", "%0A") + return r.Replace(s) +} diff --git a/feature/github-repo-importer/cmd/root_test.go b/feature/github-repo-importer/cmd/root_test.go new file mode 100644 index 0000000..022c22b --- /dev/null +++ b/feature/github-repo-importer/cmd/root_test.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEscapeAnnotation(t *testing.T) { + tests := []struct { + name string + in string + want string + }{ + { + name: "no special characters", + in: "repository not found", + want: "repository not found", + }, + { + name: "percent is escaped", + in: "50% off", + want: "50%25 off", + }, + { + name: "newline is escaped", + in: "line1\nline2", + want: "line1%0Aline2", + }, + { + name: "carriage return is escaped", + in: "line1\rline2", + want: "line1%0Dline2", + }, + { + name: "combined special characters", + in: "50% off\r\nnext line", + want: "50%25 off%0D%0Anext line", + }, + { + name: "percent is escaped before its replacement is rescanned", + in: "%0A", + want: "%250A", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, escapeAnnotation(tt.in)) + }) + } +} diff --git a/feature/github-repo-importer/pkg/github/github.go b/feature/github-repo-importer/pkg/github/github.go index fa363f7..6c94581 100644 --- a/feature/github-repo-importer/pkg/github/github.go +++ b/feature/github-repo-importer/pkg/github/github.go @@ -23,6 +23,10 @@ var ( v4client *githubv4.Client ) +// ErrRepoNotFound is returned when the repository to import does not exist or +// the authenticated GitHub App has no access to it (HTTP 404). +var ErrRepoNotFound = errors.New("repository not found") + func InitializeClients() { var err error v3client, v4client, err = CreateGitHubClient() @@ -66,7 +70,10 @@ func ImportRepo(repoName string) (*Repository, error) { repoNameSplit := strings.Split(repoName, "/") repo, r, err := v3client.Repositories.Get(context.Background(), repoNameSplit[0], repoNameSplit[1]) if err != nil { - return nil, fmt.Errorf("failed to fetch repo: %w (API Response: %s)", err, r.Status) + if r != nil && r.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%w: %q — check the spelling and that the GitHub App is installed and has access to it", ErrRepoNotFound, repoName) + } + return nil, fmt.Errorf("failed to fetch repo %q: %w", repoName, err) } if err := dumpManager.WriteJSONFile("repository.json", repo); err != nil { @@ -94,10 +101,10 @@ func ImportRepo(repoName string) (*Repository, error) { rulesets, r, err := v3client.Repositories.GetAllRulesets(context.Background(), repoNameSplit[0], repoNameSplit[1], false) if err != nil { - if r.StatusCode == http.StatusForbidden { + if r != nil && r.StatusCode == http.StatusForbidden { fmt.Printf("skipping rulesets due to insufficient permissions: %v\n", err) } else { - return nil, fmt.Errorf("failed to get all rulesets: %v", err) + return nil, fmt.Errorf("failed to get all rulesets: %w", err) } } @@ -134,9 +141,9 @@ func ImportRepo(repoName string) (*Repository, error) { fmt.Printf("failed to write branch_protection_rules.json: %v\n", err) } - orgTeams, res, err := v3client.Teams.ListTeams(context.Background(), repoNameSplit[0], &github.ListOptions{PerPage: 100}) + orgTeams, _, err := v3client.Teams.ListTeams(context.Background(), repoNameSplit[0], &github.ListOptions{PerPage: 100}) if err != nil { - return nil, fmt.Errorf("failed to get org teams: %w (API Response: %s)", err, res.Status) + return nil, fmt.Errorf("failed to get org teams: %w", err) } if err := dumpManager.WriteJSONFile("org_teams.json", orgTeams); err != nil {