diff --git a/.github/workflows/pizza.yml b/.github/workflows/pizza.yml index d4661f2..aa0b863 100644 --- a/.github/workflows/pizza.yml +++ b/.github/workflows/pizza.yml @@ -11,6 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Pizza Action - uses: open-sauced/pizza-action@v2.0.0 + uses: open-sauced/pizza-action@v2.1.0 with: commit-and-pr: "true" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index aa3e092..a49980c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -59,10 +59,46 @@ jobs: NPM_PACKAGE_ROOT: "npm" SKIP_DOCKER_PUBLISH: true + docs: + name: Update documentation + needs: + - release + + runs-on: ubuntu-latest + + steps: + - name: "Generate token" + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.OS_GITHUB_APP_ID }} + private_key: ${{ secrets.OS_GITHUB_APP_PRIVATE_KEY }} + + - name: "☁️ checkout repository" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ steps.generate_token.outputs.token }} + + - name: "🐹 Setup Go" + uses: actions/setup-go@v5 + with: + go-version: 1.22.x + + - name: "🤲 Setup Just" + uses: extractions/setup-just@v2 + + - name: "📗 Generate Documentation" + run: ./scripts/generate-docs.sh + env: + GITHUB_REF: ${{ github.ref }} + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + build: name: Build and publish artifacts needs: - release + - docs if: needs.release.outputs.release-tag != '' runs-on: ubuntu-latest permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bfa1fc..08f8fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,48 @@ > All notable changes to this project will be documented in this file +## [1.4.0-beta.4](https://github.com/open-sauced/pizza-cli/compare/v1.4.0-beta.3...v1.4.0-beta.4) (2024-09-09) + + +### 🐛 Bug Fixes + +* docs generation runs outside of build matrix now ([#165](https://github.com/open-sauced/pizza-cli/issues/165)) ([1e42988](https://github.com/open-sauced/pizza-cli/commit/1e42988c06fcab6694d4fca9670c59796352e7a5)) + +## [1.4.0-beta.3](https://github.com/open-sauced/pizza-cli/compare/v1.4.0-beta.2...v1.4.0-beta.3) (2024-09-09) + + +### 🐛 Bug Fixes + +* now --tty-disable is set so the action can complete instead of hanging ([#164](https://github.com/open-sauced/pizza-cli/issues/164)) ([a970a73](https://github.com/open-sauced/pizza-cli/commit/a970a73f494f34464a4c8b6ba993d38ecb4e2ec4)) + +## [1.4.0-beta.2](https://github.com/open-sauced/pizza-cli/compare/v1.4.0-beta.1...v1.4.0-beta.2) (2024-09-09) + + +### 🐛 Bug Fixes + +* fixed docs generation in release workflow ([#162](https://github.com/open-sauced/pizza-cli/issues/162)) ([5341e16](https://github.com/open-sauced/pizza-cli/commit/5341e16daaeeecdc664895d165246a82623accbe)) + +## [1.4.0-beta.1](https://github.com/open-sauced/pizza-cli/compare/v1.3.1-beta.2...v1.4.0-beta.1) (2024-09-09) + + +### 🍕 Features + +* Posthog events bootstrapping ([#160](https://github.com/open-sauced/pizza-cli/issues/160)) ([847426b](https://github.com/open-sauced/pizza-cli/commit/847426bcb202e8846287461fb0e3735d04f4c82e)) + +## [1.3.1-beta.2](https://github.com/open-sauced/pizza-cli/compare/v1.3.1-beta.1...v1.3.1-beta.2) (2024-09-06) + + +### 🐛 Bug Fixes + +* use the local directory and home directory as fallback for .sauced.yaml ([#158](https://github.com/open-sauced/pizza-cli/issues/158)) ([af2f361](https://github.com/open-sauced/pizza-cli/commit/af2f3612e26634455602d1840714c5bf15e1e40a)) + +## [1.3.1-beta.1](https://github.com/open-sauced/pizza-cli/compare/v1.3.0...v1.3.1-beta.1) (2024-09-06) + + +### 🐛 Bug Fixes + +* skip interactive steps in generate codeowners with --tty-disable flag ([#159](https://github.com/open-sauced/pizza-cli/issues/159)) ([49f1fd3](https://github.com/open-sauced/pizza-cli/commit/49f1fd3fc4df24b95724feb1918dc80276cd017e)) + ## [1.3.0](https://github.com/open-sauced/pizza-cli/compare/v1.2.1...v1.3.0) (2024-09-06) diff --git a/CODEOWNERS b/CODEOWNERS index a782015..ab3a50d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,40 +1,103 @@ # This file is generated automatically by OpenSauced pizza-cli. DO NOT EDIT. Stay saucy! +# +# Generated with command: +# $ pizza generate codeowners pizza-cli/ --tty-disable true +.env @jpmcb .github/ISSUE_TEMPLATE/bug_report.yaml @jpmcb .github/ISSUE_TEMPLATE/config.yaml @jpmcb .github/ISSUE_TEMPLATE/feature_request.yaml @jpmcb .github/workflows/auto-add-to-project.yml @jpmcb -.github/workflows/release.yaml @jpmcb +.github/workflows/pizza.yml @nickytonline @jpmcb +.github/workflows/release.yaml @jpmcb @nickytonline .github/workflows/test.yaml @jpmcb .golangci.yaml @jpmcb -CHANGELOG.md @jpmcb @brandonroberts @nickytonline +.sauced.yaml @nickytonline @jpmcb +CHANGELOG.md @jpmcb @nickytonline @brandonroberts +CODEOWNERS @nickytonline @jpmcb Dockerfile @jpmcb @nickytonline Makefile @jpmcb README.md @jpmcb +api/auth/auth.go @jpmcb +api/auth/success.html @nickytonline @jpmcb +api/client.go @jpmcb +api/mock/mock.go @jpmcb +api/services/contributors/contributors.go @jpmcb +api/services/contributors/contributors_test.go @jpmcb +api/services/contributors/spec.go @jpmcb +api/services/histogram/histogram.go @jpmcb +api/services/histogram/histogram_test.go @jpmcb +api/services/histogram/spec.go @jpmcb +api/services/repository/repository.go @jpmcb +api/services/repository/repository_test.go @jpmcb +api/services/repository/spec.go @jpmcb +api/services/spec.go @jpmcb +api/services/workspaces/spec.go @jpmcb +api/services/workspaces/userlists/spec.go @jpmcb +api/services/workspaces/userlists/userlists.go @jpmcb +api/services/workspaces/userlists/userlists_test.go @jpmcb +api/services/workspaces/workspaces.go @jpmcb +api/services/workspaces/workspaces_test.go @jpmcb +api/utils/validators.go @jpmcb cmd/auth/auth.go @jpmcb +cmd/auth/constants.go @jpmcb +cmd/auth/schema.go @jpmcb +cmd/auth/success.html @jpmcb +cmd/auth/success.html @jpmcb cmd/bake/bake.go @jpmcb cmd/bake/bake_test.go @jpmcb -cmd/generate/codeowners/codeowners.go @jpmcb -cmd/generate/codeowners/output.go @jpmcb @brandonroberts -cmd/generate/codeowners/output_test.go @brandonroberts +cmd/docs/docs.go @nickytonline @jpmcb +cmd/docs/docs_test.go @nickytonline @jpmcb +cmd/generate/codeowners/codeowners.go @jpmcb @nickytonline @zeucapua +cmd/generate/codeowners/output.go @jpmcb @zeucapua @brandonroberts +cmd/generate/codeowners/output_test.go @jpmcb @brandonroberts cmd/generate/codeowners/spec.go @jpmcb cmd/generate/codeowners/traversal.go @jpmcb cmd/generate/generate.go @jpmcb cmd/insights/contributors.go @jpmcb -cmd/insights/insights.go @jpmcb +cmd/insights/insights.go @jpmcb @brandonroberts cmd/insights/repositories.go @jpmcb cmd/insights/user-contributions.go @jpmcb cmd/insights/utils.go @jpmcb cmd/repo-query/repo-query.go @jpmcb -cmd/root/root.go @jpmcb +cmd/root/root.go @jpmcb @brandonroberts @nickytonline +cmd/show/constants.go @jpmcb +cmd/show/contributors.go @jpmcb +cmd/show/dashboard.go @jpmcb cmd/show/show.go @jpmcb +cmd/show/tui.go @jpmcb cmd/version/version.go @jpmcb @nickytonline -go.mod @jpmcb -go.sum @jpmcb +docs/pizza.md @nickytonline @jpmcb +docs/pizza_completion.md @nickytonline @jpmcb +docs/pizza_completion_bash.md @nickytonline @jpmcb +docs/pizza_completion_fish.md @nickytonline @jpmcb +docs/pizza_completion_powershell.md @nickytonline @jpmcb +docs/pizza_completion_zsh.md @nickytonline @jpmcb +docs/pizza_generate.md @nickytonline @jpmcb +docs/pizza_generate_codeowners.md @nickytonline @jpmcb +docs/pizza_insights.md @nickytonline @jpmcb +docs/pizza_insights_contributors.md @nickytonline @jpmcb +docs/pizza_insights_repositories.md @nickytonline @jpmcb +docs/pizza_insights_user-contributions.md @nickytonline @jpmcb +docs/pizza_login.md @nickytonline @jpmcb +docs/pizza_version.md @nickytonline @jpmcb +go.mod @jpmcb @nickytonline +go.sum @jpmcb @nickytonline justfile @jpmcb @nickytonline -npm/package.json @jpmcb @brandonroberts @nickytonline -pkg/config/config.go @jpmcb -pkg/config/spec.go @jpmcb +npm/.gitignore @jpmcb +npm/package-lock.json @jpmcb @nickytonline @brandonroberts +npm/package.json @jpmcb @nickytonline @brandonroberts +pkg/api/client.go @jpmcb +pkg/api/validation.go @jpmcb +pkg/config/config.go @jpmcb @nickytonline @brandonroberts +pkg/config/config_test.go @nickytonline @jpmcb @brandonroberts +pkg/config/file.go @jpmcb +pkg/config/spec.go @jpmcb @brandonroberts +pkg/constants/flags.go @jpmcb pkg/logging/constants.go @jpmcb +pkg/utils/posthog.go @jpmcb pkg/utils/root.go @jpmcb -pkg/utils/version.go @nickytonline +pkg/utils/telemetry.go @jpmcb +pkg/utils/version.go @nickytonline @jpmcb +scripts/generate-docs.sh @nickytonline +telemetry.go @jpmcb diff --git a/README.md b/README.md index 27f4d3e..9f4b80c 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ Flags: Global Flags: --beta Shorthand for using the beta OpenSauced API endpoint ("https://beta.api.opensauced.pizza"). Supersedes the '--endpoint' flag - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default ".sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -e, --endpoint string The API endpoint to send requests to (default "https://api.opensauced.pizza") -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 802410f..86835dd 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -35,16 +35,17 @@ func NewLoginCommand() *cobra.Command { disableTelem, _ := cmd.Flags().GetBool(constants.FlagNameTelemetry) opts.telemetry = utils.NewPosthogCliClient(!disableTelem) - defer opts.telemetry.Done() username, err := run() if err != nil { - opts.telemetry.CaptureFailedLogin() + _ = opts.telemetry.CaptureFailedLogin() } else { - opts.telemetry.CaptureLogin(username) + _ = opts.telemetry.CaptureLogin(username) } + _ = opts.telemetry.Done() + return err }, } diff --git a/cmd/generate/codeowners/codeowners.go b/cmd/generate/codeowners/codeowners.go index 4741a67..20491dd 100644 --- a/cmd/generate/codeowners/codeowners.go +++ b/cmd/generate/codeowners/codeowners.go @@ -46,21 +46,41 @@ type Options struct { config *config.Spec } -const codeownersLongDesc string = `WARNING: Proof of concept feature. +const codeownersLongDesc string = `Generates a CODEOWNERS file for a given git repository. The generated file specifies up to 3 owners for EVERY file in the git tree based on the number of lines touched in that specific file over the specified range of time. -Generates a CODEOWNERS file for a given git repository. This uses a ~/.sauced.yaml -configuration to attribute emails with given entities. +Configuration: +The command requires a .sauced.yaml file for accurate attribution. This file maps +commit email addresses to GitHub usernames. The command looks for this file in two locations: -The generated file specifies up to 3 owners for EVERY file in the git tree based on the -number of lines touched in that specific file over the specified range of time.` +1. In the root of the specified repository path +2. In the user's home directory (~/.sauced.yaml) if not found in the repository + +If you run the command on a specific path, it will first look for .sauced.yaml in that +path. If not found, it will fall back to ~/.sauced.yaml.` func NewCodeownersCommand() *cobra.Command { opts := &Options{} cmd := &cobra.Command{ Use: "codeowners path/to/repo [flags]", - Short: "Generates a CODEOWNERS file for a given repository using a \"~/.sauced.yaml\" config", + Short: "Generate a CODEOWNERS file for a GitHub repository using a \"~/.sauced.yaml\" config", Long: codeownersLongDesc, + Example: ` +# Generate CODEOWNERS file for the current directory +pizza generate codeowners . + +# Generate CODEOWNERS file for a specific repository +pizza generate codeowners /path/to/your/repo + +# Generate CODEOWNERS file analyzing the last 180 days +pizza generate codeowners . --range 180 + +# Generate an OWNERS style file instead of CODEOWNERS +pizza generate codeowners . --owners-style-file + +# Specify a custom location for the .sauced.yaml file +pizza generate codeowners . --config /path/to/.sauced.yaml + `, Args: func(_ *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("you must provide exactly one argument: the path to the repository") @@ -87,10 +107,9 @@ func NewCodeownersCommand() *cobra.Command { disableTelem, _ := cmd.Flags().GetBool(constants.FlagNameTelemetry) opts.telemetry = utils.NewPosthogCliClient(!disableTelem) - defer opts.telemetry.Done() configPath, _ := cmd.Flags().GetString("config") - opts.config, err = config.LoadConfig(configPath, filepath.Join(opts.path, ".sauced.yaml")) + opts.config, err = config.LoadConfig(configPath) if err != nil { return err } @@ -112,12 +131,16 @@ func NewCodeownersCommand() *cobra.Command { opts.loglevel = logging.LogDebug } - return run(opts, cmd) + err = run(opts, cmd) + + _ = opts.telemetry.Done() + + return err }, } - cmd.PersistentFlags().IntP("range", "r", 90, "The number of days to lookback") - cmd.PersistentFlags().Bool("owners-style-file", false, "Whether to generate an agnostic OWNERS style file.") + cmd.PersistentFlags().IntP("range", "r", 90, "The number of days to analyze commit history (default 90)") + cmd.PersistentFlags().Bool("owners-style-file", false, "Generate an agnostic OWNERS style file instead of CODEOWNERS.") return cmd } @@ -135,7 +158,7 @@ func run(opts *Options, cmd *cobra.Command) error { repo, err := git.PlainOpen(opts.path) if err != nil { - opts.telemetry.CaptureFailedCodeownersGenerate() + _ = opts.telemetry.CaptureFailedCodeownersGenerate() return fmt.Errorf("error opening repo: %w", err) } opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Opened repo at: %s\n", opts.path) @@ -150,7 +173,7 @@ func run(opts *Options, cmd *cobra.Command) error { codeowners, err := processOptions.process() if err != nil { - opts.telemetry.CaptureFailedCodeownersGenerate() + _ = opts.telemetry.CaptureFailedCodeownersGenerate() return fmt.Errorf("error traversing git log: %w", err) } @@ -165,11 +188,16 @@ func run(opts *Options, cmd *cobra.Command) error { opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Processing codeowners file at: %s\n", outputPath) err = generateOutputFile(codeowners, outputPath, opts, cmd) if err != nil { - opts.telemetry.CaptureFailedCodeownersGenerate() + _ = opts.telemetry.CaptureFailedCodeownersGenerate() return fmt.Errorf("error generating github style codeowners file: %w", err) } opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Finished generating file: %s\n", outputPath) - opts.telemetry.CaptureCodeownersGenerate() + _ = opts.telemetry.CaptureCodeownersGenerate() + + // ignore the interactive prompts for CI/CD environments + if opts.tty { + return nil + } // 1. Ask if they want to add users to a list var input string @@ -204,11 +232,11 @@ func run(opts *Options, cmd *cobra.Command) error { case "y", "Y", "yes": user, err := authenticator.Login() if err != nil { - opts.telemetry.CaptureFailedCodeownersGenerateAuth() + _ = opts.telemetry.CaptureFailedCodeownersGenerateAuth() opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error logging in\n") return fmt.Errorf("could not log in: %w", err) } - opts.telemetry.CaptureCodeownersGenerateAuth(user) + _ = opts.telemetry.CaptureCodeownersGenerateAuth(user) opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Logged in as: %s\n", user) case "n", "N", "no": @@ -221,7 +249,7 @@ func run(opts *Options, cmd *cobra.Command) error { opts.token, err = authenticator.GetSessionToken() if err != nil { - opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight() + _ = opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight() opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error getting session token\n") return fmt.Errorf("could not get session token: %w", err) } @@ -231,7 +259,7 @@ func run(opts *Options, cmd *cobra.Command) error { opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Looking up OpenSauced workspace: Pizza CLI\n") workspace, err := findCreatePizzaCliWorkspace(opts) if err != nil { - opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight() + _ = opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight() opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error finding Workspace: Pizza CLI\n") return fmt.Errorf("could not find Pizza CLI workspace: %w", err) } @@ -240,13 +268,13 @@ func run(opts *Options, cmd *cobra.Command) error { opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Looking up Contributor Insight for local repository: %s\n", listName) userList, err := updateCreateLocalWorkspaceUserList(opts, listName, workspace, codeowners) if err != nil { - opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight() + _ = opts.telemetry.CaptureFailedCodeownersGenerateContributorInsight() opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Error finding Workspace Contributor Insight: %s\n", listName) return fmt.Errorf("could not find Workspace Contributor Insight: %s - %w", listName, err) } opts.logger.V(logging.LogDebug).Style(0, colors.FgGreen).Infof("Updated Contributor Insight for local repository: %s\n", listName) opts.logger.V(logging.LogInfo).Style(0, colors.FgCyan).Infof("Access list on OpenSauced:\n%s\n", fmt.Sprintf("https://app.opensauced.pizza/workspaces/%s/contributor-insights/%s", workspace.ID, userList.ID)) - opts.telemetry.CaptureCodeownersGenerateContributorInsight() + _ = opts.telemetry.CaptureCodeownersGenerateContributorInsight() return nil } diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go new file mode 100644 index 0000000..e5e3e19 --- /dev/null +++ b/cmd/generate/config/config.go @@ -0,0 +1,137 @@ +package config + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "slices" + + tea "github.com/charmbracelet/bubbletea" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/spf13/cobra" +) + +// Options for the config generation command +type Options struct { + // the path to the git repository on disk to generate a codeowners file for + path string + + // where the '.sauced.yaml' file will go + outputPath string + + // whether to use interactive mode + isInteractive bool + + // from global config + ttyDisabled bool +} + +const configLongDesc string = `Generates a ".sauced.yaml" configuration file. The attribution of emails to given entities +is based on the repository this command is ran in.` + +func NewConfigCommand() *cobra.Command { + opts := &Options{} + + cmd := &cobra.Command{ + Use: "config path/to/repo [flags]", + Short: "Generates a \".sauced.yaml\" config based on the current repository", + Long: configLongDesc, + Args: func(_ *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("you must provide exactly one argument: the path to the repository") + } + + path := args[0] + + // Validate that the path is a real path on disk and accessible by the user + absPath, err := filepath.Abs(path) + if err != nil { + return err + } + + if _, err := os.Stat(absPath); os.IsNotExist(err) { + return fmt.Errorf("the provided path does not exist: %w", err) + } + + opts.path = absPath + return nil + }, + + RunE: func(cmd *cobra.Command, _ []string) error { + opts.outputPath, _ = cmd.Flags().GetString("output-path") + opts.isInteractive, _ = cmd.Flags().GetBool("interactive") + opts.ttyDisabled, _ = cmd.Flags().GetBool("tty-disable") + + return run(opts) + }, + } + + cmd.PersistentFlags().StringP("output-path", "o", "./", "Directory to create the `.sauced.yaml` file.") + cmd.PersistentFlags().BoolP("interactive", "i", false, "Whether to be interactive") + return cmd +} + +func run(opts *Options) error { + attributionMap := make(map[string][]string) + + // Open repo + repo, err := git.PlainOpen(opts.path) + if err != nil { + return fmt.Errorf("error opening repo: %w", err) + } + + commitIter, err := repo.CommitObjects() + + if err != nil { + return fmt.Errorf("error opening repo commits: %w", err) + } + + var uniqueEmails []string + err = commitIter.ForEach(func(c *object.Commit) error { + name := c.Author.Name + email := c.Author.Email + + if opts.ttyDisabled || !opts.isInteractive { + doesEmailExist := slices.Contains(attributionMap[name], email) + if !doesEmailExist { + // AUTOMATIC: set every name and associated emails + attributionMap[name] = append(attributionMap[name], email) + } + } else if !slices.Contains(uniqueEmails, email) { + uniqueEmails = append(uniqueEmails, email) + } + return nil + }) + + if err != nil { + return fmt.Errorf("error iterating over repo commits: %w", err) + } + + // INTERACTIVE: per unique email, set a name (existing or new or ignore) + if opts.isInteractive && !opts.ttyDisabled { + program := tea.NewProgram(initialModel(opts, uniqueEmails)) + if _, err := program.Run(); err != nil { + return fmt.Errorf("error running interactive mode: %w", err) + } + } else { + // generate an output file + // default: `./.sauced.yaml` + // fallback for home directories + if opts.outputPath == "~/" { + homeDir, _ := os.UserHomeDir() + err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("error generating output file: %w", err) + } + } else { + err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("error generating output file: %w", err) + } + } + } + + return nil +} diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go new file mode 100644 index 0000000..f8db065 --- /dev/null +++ b/cmd/generate/config/output.go @@ -0,0 +1,35 @@ +package config + +import ( + "fmt" + "os" + + "github.com/open-sauced/pizza-cli/pkg/config" + "github.com/open-sauced/pizza-cli/pkg/utils" +) + +func generateOutputFile(outputPath string, attributionMap map[string][]string) error { + file, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("error creating %s file: %w", outputPath, err) + } + defer file.Close() + + var config config.Spec + config.Attributions = attributionMap + + // for pretty print test + yaml, err := utils.OutputYAML(config) + + if err != nil { + return fmt.Errorf("failed to turn into YAML: %w", err) + } + + _, err = file.WriteString(yaml) + + if err != nil { + return fmt.Errorf("failed to turn into YAML: %w", err) + } + + return nil +} diff --git a/cmd/generate/config/spec.go b/cmd/generate/config/spec.go new file mode 100644 index 0000000..5284cc9 --- /dev/null +++ b/cmd/generate/config/spec.go @@ -0,0 +1,145 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +// Bubbletea for Interactive Mode + +type model struct { + textInput textinput.Model + help help.Model + keymap keymap + + opts *Options + attributionMap map[string][]string + uniqueEmails []string + currentIndex int +} + +type keymap struct{} + +func (k keymap) ShortHelp() []key.Binding { + return []key.Binding{ + key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next suggestion")), + key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev suggestion")), + key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")), + key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")), + key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), + } +} + +func (k keymap) FullHelp() [][]key.Binding { + return [][]key.Binding{k.ShortHelp()} +} + +func initialModel(opts *Options, uniqueEmails []string) model { + ti := textinput.New() + ti.Placeholder = "username" + ti.Focus() + ti.ShowSuggestions = true + + return model{ + textInput: ti, + help: help.New(), + keymap: keymap{}, + + opts: opts, + attributionMap: make(map[string][]string), + uniqueEmails: uniqueEmails, + currentIndex: 0, + } +} + +func (m model) Init() tea.Cmd { + return textinput.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + currentEmail := m.uniqueEmails[m.currentIndex] + + existingUsers := make([]string, 0, len(m.attributionMap)) + for k := range m.attributionMap { + existingUsers = append(existingUsers, k) + } + + m.textInput.SetSuggestions(existingUsers) + + keyMsg, ok := msg.(tea.KeyMsg) + + if ok { + switch keyMsg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + + case tea.KeyCtrlI: + m.currentIndex++ + if m.currentIndex+1 >= len(m.uniqueEmails) { + return m, runOutputGeneration(m.opts, m.attributionMap) + } + return m, nil + + case tea.KeyEnter: + if len(strings.Trim(m.textInput.Value(), " ")) == 0 { + return m, nil + } + m.attributionMap[m.textInput.Value()] = append(m.attributionMap[m.textInput.Value()], currentEmail) + m.textInput.Reset() + if m.currentIndex+1 >= len(m.uniqueEmails) { + return m, runOutputGeneration(m.opts, m.attributionMap) + } + + m.currentIndex++ + return m, nil + } + } + + m.textInput, cmd = m.textInput.Update(msg) + + return m, cmd +} + +func (m model) View() string { + currentEmail := "" + if m.currentIndex < len(m.uniqueEmails) { + currentEmail = m.uniqueEmails[m.currentIndex] + } + + return fmt.Sprintf( + "Found email %s - who to attribute to?: \n%s\n\n%s\n", + currentEmail, + m.textInput.View(), + m.help.View(m.keymap), + ) +} + +func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.Cmd { + // generate an output file + // default: `./.sauced.yaml` + // fallback for home directories + return func() tea.Msg { + if opts.outputPath == "~/" { + homeDir, _ := os.UserHomeDir() + err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("error generating output file: %w", err) + } + } else { + err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("error generating output file: %w", err) + } + } + + return tea.Quit() + } +} diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go index b3ff321..13d7396 100644 --- a/cmd/generate/generate.go +++ b/cmd/generate/generate.go @@ -6,16 +6,15 @@ import ( "github.com/spf13/cobra" "github.com/open-sauced/pizza-cli/cmd/generate/codeowners" + "github.com/open-sauced/pizza-cli/cmd/generate/config" ) -const generateLongDesc string = `WARNING: Proof of concept feature. - -XXX` +const generateLongDesc string = `The 'generate' command provides tools to automate the creation of important project documentation and derive insights from your codebase.` func NewGenerateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "generate [subcommand] [flags]", - Short: "Generates something", + Short: "Generates documentation and insights from your codebase", Long: generateLongDesc, Args: func(_ *cobra.Command, args []string) error { if len(args) != 1 { @@ -28,6 +27,7 @@ func NewGenerateCommand() *cobra.Command { } cmd.AddCommand(codeowners.NewCodeownersCommand()) + cmd.AddCommand(config.NewConfigCommand()) return cmd } diff --git a/cmd/insights/contributors.go b/cmd/insights/contributors.go index b1103e4..031d033 100644 --- a/cmd/insights/contributors.go +++ b/cmd/insights/contributors.go @@ -57,7 +57,6 @@ func NewContributorsCommand() *cobra.Command { disableTelem, _ := cmd.Flags().GetBool(constants.FlagNameTelemetry) opts.telemetry = utils.NewPosthogCliClient(!disableTelem) - defer opts.telemetry.Done() endpointURL, _ := cmd.Flags().GetString(constants.FlagNameEndpoint) opts.APIClient = api.NewClient(endpointURL) @@ -67,11 +66,13 @@ func NewContributorsCommand() *cobra.Command { err := opts.run() if err != nil { - opts.telemetry.CaptureInsights() + _ = opts.telemetry.CaptureInsights() } else { - opts.telemetry.CaptureFailedInsights() + _ = opts.telemetry.CaptureFailedInsights() } + _ = opts.telemetry.Done() + return err }, } diff --git a/cmd/insights/repositories.go b/cmd/insights/repositories.go index a86a380..ca4a1fe 100644 --- a/cmd/insights/repositories.go +++ b/cmd/insights/repositories.go @@ -57,7 +57,6 @@ func NewRepositoriesCommand() *cobra.Command { disableTelem, _ := cmd.Flags().GetBool(constants.FlagNameTelemetry) opts.telemetry = utils.NewPosthogCliClient(!disableTelem) - defer opts.telemetry.Done() endpointURL, _ := cmd.Flags().GetString(constants.FlagNameEndpoint) opts.APIClient = api.NewClient(endpointURL) @@ -67,11 +66,13 @@ func NewRepositoriesCommand() *cobra.Command { err := opts.run() if err != nil { - opts.telemetry.CaptureInsights() + _ = opts.telemetry.CaptureInsights() } else { - opts.telemetry.CaptureFailedInsights() + _ = opts.telemetry.CaptureFailedInsights() } + _ = opts.telemetry.Done() + return err }, } diff --git a/cmd/root/root.go b/cmd/root/root.go index 379baaa..7b35e4a 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -19,7 +19,7 @@ func NewRootCommand() (*cobra.Command, error) { cmd := &cobra.Command{ Use: "pizza [flags]", Short: "OpenSauced CLI", - Long: "A command line utility for insights, metrics, and all things OpenSauced", + Long: "A command line utility for insights, metrics, and generating CODEOWNERS documentation for your open source projects", RunE: run, Args: func(cmd *cobra.Command, _ []string) error { betaFlag := cmd.Flags().Lookup(constants.FlagNameBeta) @@ -36,7 +36,7 @@ func NewRootCommand() (*cobra.Command, error) { cmd.PersistentFlags().StringP(constants.FlagNameEndpoint, "e", constants.EndpointProd, "The API endpoint to send requests to") cmd.PersistentFlags().Bool(constants.FlagNameBeta, false, fmt.Sprintf("Shorthand for using the beta OpenSauced API endpoint (\"%s\"). Supersedes the '--%s' flag", constants.EndpointBeta, constants.FlagNameEndpoint)) cmd.PersistentFlags().Bool(constants.FlagNameTelemetry, false, "Disable sending telemetry data to OpenSauced") - cmd.PersistentFlags().StringP("config", "c", "~/.sauced.yaml", "The saucectl config") + cmd.PersistentFlags().StringP("config", "c", "~/.sauced.yaml", "The codeowners config") cmd.PersistentFlags().StringP("log-level", "l", "info", "The logging level. Options: error, warn, info, debug") cmd.PersistentFlags().Bool("tty-disable", false, "Disable log stylization. Suitable for CI/CD and automation") diff --git a/docs/pizza.md b/docs/pizza.md index 49abe05..58ea659 100644 --- a/docs/pizza.md +++ b/docs/pizza.md @@ -4,7 +4,7 @@ OpenSauced CLI ### Synopsis -A command line utility for insights, metrics, and all things OpenSauced +A command line utility for insights, metrics, and generating CODEOWNERS documentation for your open source projects ``` pizza [flags] @@ -13,7 +13,7 @@ pizza [flags] ### Options ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -h, --help help for pizza -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") @@ -23,7 +23,7 @@ pizza [flags] ### SEE ALSO * [pizza completion](pizza_completion.md) - Generate the autocompletion script for the specified shell -* [pizza generate](pizza_generate.md) - Generates something +* [pizza generate](pizza_generate.md) - Generates documentation and insights from your codebase * [pizza insights](pizza_insights.md) - Gather insights about git contributors, repositories, users and pull requests * [pizza login](pizza_login.md) - Log into the CLI via GitHub * [pizza version](pizza_version.md) - Displays the build version of the CLI diff --git a/docs/pizza_completion.md b/docs/pizza_completion.md index 65a7507..1506e1b 100644 --- a/docs/pizza_completion.md +++ b/docs/pizza_completion.md @@ -17,7 +17,7 @@ See each sub-command's help for details on how to use the generated script. ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation diff --git a/docs/pizza_completion_bash.md b/docs/pizza_completion_bash.md index 5beed9d..3a32c97 100644 --- a/docs/pizza_completion_bash.md +++ b/docs/pizza_completion_bash.md @@ -40,7 +40,7 @@ pizza completion bash ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation diff --git a/docs/pizza_completion_fish.md b/docs/pizza_completion_fish.md index 93bb9de..734d1e0 100644 --- a/docs/pizza_completion_fish.md +++ b/docs/pizza_completion_fish.md @@ -31,7 +31,7 @@ pizza completion fish [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation diff --git a/docs/pizza_completion_powershell.md b/docs/pizza_completion_powershell.md index bec9a87..06b2036 100644 --- a/docs/pizza_completion_powershell.md +++ b/docs/pizza_completion_powershell.md @@ -28,7 +28,7 @@ pizza completion powershell [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation diff --git a/docs/pizza_completion_zsh.md b/docs/pizza_completion_zsh.md index 22b1734..5e45162 100644 --- a/docs/pizza_completion_zsh.md +++ b/docs/pizza_completion_zsh.md @@ -42,7 +42,7 @@ pizza completion zsh [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation diff --git a/docs/pizza_generate.md b/docs/pizza_generate.md index edd658d..61b0e8e 100644 --- a/docs/pizza_generate.md +++ b/docs/pizza_generate.md @@ -1,12 +1,10 @@ ## pizza generate -Generates something +Generates documentation and insights from your codebase ### Synopsis -WARNING: Proof of concept feature. - -XXX +The 'generate' command provides tools to automate the creation of important project documentation and derive insights from your codebase. ``` pizza generate [subcommand] [flags] @@ -21,7 +19,7 @@ pizza generate [subcommand] [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation @@ -30,5 +28,6 @@ pizza generate [subcommand] [flags] ### SEE ALSO * [pizza](pizza.md) - OpenSauced CLI -* [pizza generate codeowners](pizza_generate_codeowners.md) - Generates a CODEOWNERS file for a given repository using a "~/.sauced.yaml" config +* [pizza generate codeowners](pizza_generate_codeowners.md) - Generate a CODEOWNERS file for a GitHub repository using a "~/.sauced.yaml" config +* [pizza generate config](pizza_generate_config.md) - Generates a ".sauced.yaml" config based on the current repository diff --git a/docs/pizza_generate_codeowners.md b/docs/pizza_generate_codeowners.md index 0cb8357..dcbbfd0 100644 --- a/docs/pizza_generate_codeowners.md +++ b/docs/pizza_generate_codeowners.md @@ -1,33 +1,58 @@ ## pizza generate codeowners -Generates a CODEOWNERS file for a given repository using a "~/.sauced.yaml" config +Generate a CODEOWNERS file for a GitHub repository using a "~/.sauced.yaml" config ### Synopsis -WARNING: Proof of concept feature. +Generates a CODEOWNERS file for a given git repository. The generated file specifies up to 3 owners for EVERY file in the git tree based on the number of lines touched in that specific file over the specified range of time. -Generates a CODEOWNERS file for a given git repository. This uses a ~/.sauced.yaml -configuration to attribute emails with given entities. +Configuration: +The command requires a .sauced.yaml file for accurate attribution. This file maps +commit email addresses to GitHub usernames. The command looks for this file in two locations: -The generated file specifies up to 3 owners for EVERY file in the git tree based on the -number of lines touched in that specific file over the specified range of time. +1. In the root of the specified repository path +2. In the user's home directory (~/.sauced.yaml) if not found in the repository + +If you run the command on a specific path, it will first look for .sauced.yaml in that +path. If not found, it will fall back to ~/.sauced.yaml. ``` pizza generate codeowners path/to/repo [flags] ``` +### Examples + +``` + +# Generate CODEOWNERS file for the current directory +pizza generate codeowners . + +# Generate CODEOWNERS file for a specific repository +pizza generate codeowners /path/to/your/repo + +# Generate CODEOWNERS file analyzing the last 180 days +pizza generate codeowners . --range 180 + +# Generate an OWNERS style file instead of CODEOWNERS +pizza generate codeowners . --owners-style-file + +# Specify a custom location for the .sauced.yaml file +pizza generate codeowners . --config /path/to/.sauced.yaml + +``` + ### Options ``` -h, --help help for codeowners - --owners-style-file Whether to generate an agnostic OWNERS style file. - -r, --range int The number of days to lookback (default 90) + --owners-style-file Generate an agnostic OWNERS style file instead of CODEOWNERS. + -r, --range int The number of days to analyze commit history (default 90) (default 90) ``` ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation @@ -35,5 +60,5 @@ pizza generate codeowners path/to/repo [flags] ### SEE ALSO -* [pizza generate](pizza_generate.md) - Generates something +* [pizza generate](pizza_generate.md) - Generates documentation and insights from your codebase diff --git a/docs/pizza_generate_config.md b/docs/pizza_generate_config.md new file mode 100644 index 0000000..0366cad --- /dev/null +++ b/docs/pizza_generate_config.md @@ -0,0 +1,34 @@ +## pizza generate config + +Generates a ".sauced.yaml" config based on the current repository + +### Synopsis + +Generates a ".sauced.yaml" configuration file. The attribution of emails to given entities +is based on the repository this command is ran in. + +``` +pizza generate config path/to/repo [flags] +``` + +### Options + +``` + -h, --help help for config + -i, --interactive Whether to be interactive + -o, --output-path .sauced.yaml Directory to create the .sauced.yaml file. (default "./") +``` + +### Options inherited from parent commands + +``` + -c, --config string The codeowners config (default "~/.sauced.yaml") + --disable-telemetry Disable sending telemetry data to OpenSauced + -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") + --tty-disable Disable log stylization. Suitable for CI/CD and automation +``` + +### SEE ALSO + +* [pizza generate](pizza_generate.md) - Generates documentation and insights from your codebase + diff --git a/docs/pizza_insights.md b/docs/pizza_insights.md index c7be12d..7933480 100644 --- a/docs/pizza_insights.md +++ b/docs/pizza_insights.md @@ -20,7 +20,7 @@ pizza insights [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation diff --git a/docs/pizza_insights_contributors.md b/docs/pizza_insights_contributors.md index ce13c4e..6c9d868 100644 --- a/docs/pizza_insights_contributors.md +++ b/docs/pizza_insights_contributors.md @@ -21,7 +21,7 @@ pizza insights contributors url... [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") -o, --output string The formatting for command output. One of: (table, yaml, csv, json) (default "table") diff --git a/docs/pizza_insights_repositories.md b/docs/pizza_insights_repositories.md index 64070d4..9754648 100644 --- a/docs/pizza_insights_repositories.md +++ b/docs/pizza_insights_repositories.md @@ -21,7 +21,7 @@ pizza insights repositories url... [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") -o, --output string The formatting for command output. One of: (table, yaml, csv, json) (default "table") diff --git a/docs/pizza_insights_user-contributions.md b/docs/pizza_insights_user-contributions.md index 5ec8b0b..8f07c9d 100644 --- a/docs/pizza_insights_user-contributions.md +++ b/docs/pizza_insights_user-contributions.md @@ -23,7 +23,7 @@ pizza insights user-contributions url... [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") -o, --output string The formatting for command output. One of: (table, yaml, csv, json) (default "table") diff --git a/docs/pizza_login.md b/docs/pizza_login.md index 7ca5124..54ead8f 100644 --- a/docs/pizza_login.md +++ b/docs/pizza_login.md @@ -22,7 +22,7 @@ pizza login [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation diff --git a/docs/pizza_version.md b/docs/pizza_version.md index 5848855..ce6c725 100644 --- a/docs/pizza_version.md +++ b/docs/pizza_version.md @@ -15,7 +15,7 @@ pizza version [flags] ### Options inherited from parent commands ``` - -c, --config string The saucectl config (default "~/.sauced.yaml") + -c, --config string The codeowners config (default "~/.sauced.yaml") --disable-telemetry Disable sending telemetry data to OpenSauced -l, --log-level string The logging level. Options: error, warn, info, debug (default "info") --tty-disable Disable log stylization. Suitable for CI/CD and automation diff --git a/go.mod b/go.mod index bc698f7..225b9ea 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.22 require ( github.com/charmbracelet/bubbles v0.19.0 + github.com/charmbracelet/bubbletea v0.27.1 github.com/charmbracelet/lipgloss v0.13.0 github.com/cli/browser v1.3.0 github.com/go-git/go-git/v5 v5.12.0 github.com/jpmcb/gopherlogs v0.2.0 - github.com/posthog/posthog-go v1.2.19 + github.com/posthog/posthog-go v1.2.21 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 @@ -17,7 +18,7 @@ require ( ) require ( - github.com/charmbracelet/bubbletea v0.27.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect ) @@ -40,6 +41,7 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/uuid v1.6.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect diff --git a/go.sum b/go.sum index e0f49bc..ed93523 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= @@ -64,6 +66,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -103,6 +107,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posthog/posthog-go v1.2.19 h1:0udGG2do4LjOzE0D/ik7S3uM2wwFKwzSqswBfdcQ1y4= github.com/posthog/posthog-go v1.2.19/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM= +github.com/posthog/posthog-go v1.2.21 h1:p2ea0l+Qwtk+VC2LCAI87Dz36vwj9i+QHw5s6CpRikA= +github.com/posthog/posthog-go v1.2.21/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= diff --git a/justfile b/justfile index 79f06de..a5b8a10 100644 --- a/justfile +++ b/justfile @@ -203,3 +203,19 @@ gen-docs: # Runs all the dev tasks (like formatting, linting, building, etc.) dev: format lint test build-all + +# Calls the various Posthog capture events to add the Insights to the database +bootstrap-telemetry: + #!/usr/bin/env sh + echo "Building telemetry-oneshot" + + go build \ + -tags telemetry \ + -ldflags="-s -w" \ + -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ + -o build/telemetry-oneshot \ + telemetry.go + + ./build/telemetry-oneshot + + rm ./build/telemetry-oneshot diff --git a/npm/package-lock.json b/npm/package-lock.json index 4d6af96..310be49 100644 --- a/npm/package-lock.json +++ b/npm/package-lock.json @@ -1,12 +1,12 @@ { "name": "pizza", - "version": "1.3.0", + "version": "1.4.0-beta.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pizza", - "version": "1.3.0", + "version": "1.4.0-beta.4", "hasInstallScript": true, "license": "MIT", "bin": { diff --git a/npm/package.json b/npm/package.json index 8a0d82d..bbda5d2 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "pizza", - "version": "1.3.0", + "version": "1.4.0-beta.4", "description": "A command line utility for insights, metrics, and all things OpenSauced", "repository": "https://github.com/open-sauced/pizza-cli", "license": "MIT", diff --git a/pkg/config/config.go b/pkg/config/config.go index 1bc53ba..ed722c5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -9,27 +9,15 @@ import ( "gopkg.in/yaml.v3" ) -const DefaultConfigPath = "~/.sauced.yaml" - // LoadConfig loads a configuration file at a given path. It attempts to load -// the default location of a ".sauced.yaml" in the user's home directory if an -// empty path is provided. If none is found in the user's home directory, it tries to load -// ".sauced.yaml" from the fallback path, which is the root path of a repository. -func LoadConfig(path string, repoRootPathConfig string) (*Spec, error) { +// the default location of a ".sauced.yaml" in the current working directory if an +// empty path is provided. If none is found, it tries to load +// "~/.sauced.yaml" from the fallback path, which is the user's home directory. +func LoadConfig(path string) (*Spec, error) { println("Config path loading from -c flag", path) config := &Spec{} - if path == DefaultConfigPath || path == "" { - // load the default file path under the user's home dir - usr, err := user.Current() - if err != nil { - return nil, fmt.Errorf("could not get user home directory: %w", err) - } - - path = filepath.Join(usr.HomeDir, ".sauced.yaml") - } - absPath, err := filepath.Abs(path) if err != nil { return nil, fmt.Errorf("error resolving absolute path: %w", err) @@ -39,15 +27,27 @@ func LoadConfig(path string, repoRootPathConfig string) (*Spec, error) { if err != nil { // If the file does not exist, check if the fallback path exists if os.IsNotExist(err) { - _, err = os.Stat(repoRootPathConfig) + // load the default file path under the user's home dir + usr, err := user.Current() + if err != nil { - return nil, fmt.Errorf("error reading config file from %s or %s", absPath, repoRootPathConfig) + return nil, fmt.Errorf("could not get user home directory: %w", err) } - data, err = os.ReadFile(repoRootPathConfig) + homeDirPathConfig, err := filepath.Abs(filepath.Join(usr.HomeDir, ".sauced.yaml")) + + if err != nil { + return nil, fmt.Errorf("error home directory absolute path: %w", err) + } + + _, err = os.Stat(homeDirPathConfig) + if err != nil { + return nil, fmt.Errorf("error reading config file from %s", homeDirPathConfig) + } + data, err = os.ReadFile(homeDirPathConfig) if err != nil { - return nil, fmt.Errorf("error reading config file from %s", repoRootPathConfig) + return nil, fmt.Errorf("error reading config file from %s or %s", absPath, homeDirPathConfig) } } else { return nil, fmt.Errorf("error reading config file: %w", err) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index bec5564..c0da2e6 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -30,7 +30,7 @@ attribution: require.NoError(t, os.WriteFile(configFilePath, []byte(fileContents), 0600)) - config, err := LoadConfig(configFilePath, "") + config, err := LoadConfig(configFilePath) require.NoError(t, err) assert.NotNil(t, config) @@ -47,12 +47,12 @@ attribution: tmpDir := t.TempDir() nonExistentPath := filepath.Join(tmpDir, ".sauced.yaml") - config, err := LoadConfig(nonExistentPath, "") + config, err := LoadConfig(nonExistentPath) require.Error(t, err) assert.Nil(t, config) }) - t.Run("Non-existent file with fallback", func(t *testing.T) { + t.Run("Providing a custom .sauced.yaml location", func(t *testing.T) { t.Parallel() fileContents := `# Configuration for attributing commits with emails to GitHub user profiles # Used during codeowners generation. @@ -78,9 +78,7 @@ attribution: _, err := os.ReadFile(fallbackPath) require.NoError(t, err) - nonExistentPath := filepath.Join(tmpDir, "non-existent.yaml") - - config, err := LoadConfig(nonExistentPath, fallbackPath) + config, err := LoadConfig(fallbackPath) require.NoError(t, err) assert.NotNil(t, config) diff --git a/pkg/utils/posthog.go b/pkg/utils/posthog.go index ac66728..6383c10 100644 --- a/pkg/utils/posthog.go +++ b/pkg/utils/posthog.go @@ -1,19 +1,27 @@ package utils import ( + "fmt" + "github.com/posthog/posthog-go" ) var ( writeOnlyPublicPosthogKey = "dev" - posthogEndpoint = "https://app.posthog.com" + posthogEndpoint = "https://us.i.posthog.com" ) // PosthogCliClient is a wrapper around the posthog-go client and is used as a // API entrypoint for sending OpenSauced telemetry data for CLI commands type PosthogCliClient struct { - client posthog.Client + // client is the Posthog Go client + client posthog.Client + + // activated denotes if the user has enabled or disabled telemetry activated bool + + // uniqueID is the user's unique, anonymous identifier + uniqueID string } // NewPosthogCliClient returns a PosthogCliClient which can be used to capture @@ -32,120 +40,142 @@ func NewPosthogCliClient(activated bool) *PosthogCliClient { panic(err) } + uniqueID, err := getOrCreateUniqueID() + if err != nil { + fmt.Printf("could not build anonymous telemetry client: %s\n", err) + } + return &PosthogCliClient{ client: client, activated: activated, + uniqueID: uniqueID, } } // Done should always be called in order to flush the Posthog buffers before // the CLI exits to ensure all events are accurately captured. -// -//nolint:errcheck -func (p *PosthogCliClient) Done() { - p.client.Close() +func (p *PosthogCliClient) Done() error { + return p.client.Close() } // CaptureLogin gathers telemetry on users who log into OpenSauced via the CLI -// -//nolint:errcheck -func (p *PosthogCliClient) CaptureLogin(username string) { +func (p *PosthogCliClient) CaptureLogin(username string) error { if p.activated { - p.client.Enqueue(posthog.Capture{ + return p.client.Enqueue(posthog.Capture{ DistinctId: username, - Event: "cli_user logged in", + Event: "pizza_cli_user_logged_in", }) } + + return nil } // CaptureFailedLogin gathers telemetry on failed logins via the CLI -// -//nolint:errcheck -func (p *PosthogCliClient) CaptureFailedLogin() { +func (p *PosthogCliClient) CaptureFailedLogin() error { if p.activated { - p.client.Enqueue(posthog.Capture{ - DistinctId: "login-failures", - Event: "cli_user failed log in", + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_user_failed_log_in", }) } + + return nil } -//nolint:errcheck -func (p *PosthogCliClient) CaptureCodeownersGenerate() { +// CaptureCodeownersGenerate gathers telemetry on successful codeowners generation +func (p *PosthogCliClient) CaptureCodeownersGenerate() error { if p.activated { - p.client.Enqueue(posthog.Capture{ - DistinctId: "codeowners-generated", - Event: "cli generated codeowners", + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_generated_codeowners", }) } + + return nil } -//nolint:errcheck -func (p *PosthogCliClient) CaptureFailedCodeownersGenerate() { +// CaptureFailedCodeownersGenerate gathers telemetry on failed codeowners generation +func (p *PosthogCliClient) CaptureFailedCodeownersGenerate() error { if p.activated { - p.client.Enqueue(posthog.Capture{ - DistinctId: "failed-codeowners-generated", - Event: "cli failed to generate codeowners", + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_failed_to_generate_codeowners", }) } + + return nil } -//nolint:errcheck -func (p *PosthogCliClient) CaptureCodeownersGenerateAuth(username string) { +// CaptureCodeownersGenerateAuth gathers telemetry on successful auth flows during codeowners generation +func (p *PosthogCliClient) CaptureCodeownersGenerateAuth(username string) error { if p.activated { - p.client.Enqueue(posthog.Capture{ + return p.client.Enqueue(posthog.Capture{ DistinctId: username, - Event: "user authenticated during generate codeowners flow", + Event: "pizza_cli_user_authenticated_during_generate_codeowners_flow", }) } + + return nil } -//nolint:errcheck -func (p *PosthogCliClient) CaptureFailedCodeownersGenerateAuth() { +// CaptureFailedCodeownersGenerateAuth gathers telemetry on failed auth flows during codeowners generations +func (p *PosthogCliClient) CaptureFailedCodeownersGenerateAuth() error { if p.activated { - p.client.Enqueue(posthog.Capture{ - DistinctId: "codeowners-generate-auth-failed", - Event: "user failed to authenticate during generate codeowners flow", + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_user_failed_to_authenticate_during_generate_codeowners_flow", }) } + + return nil } -//nolint:errcheck -func (p *PosthogCliClient) CaptureCodeownersGenerateContributorInsight() { +// CaptureCodeownersGenerateContributorInsight gathers telemetry on successful +// Contributor Insights creation/update during codeowners generation +func (p *PosthogCliClient) CaptureCodeownersGenerateContributorInsight() error { if p.activated { - p.client.Enqueue(posthog.Capture{ - DistinctId: "codeowners-generate-contributor-insight", - Event: "cli created/updated contributor list for user", + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_created_updated_contributor_list", }) } + + return nil } -//nolint:errcheck -func (p *PosthogCliClient) CaptureFailedCodeownersGenerateContributorInsight() { +// CaptureFailedCodeownersGenerateContributorInsight gathers telemetry on failed +// Contributor Insights during codeowners generation +func (p *PosthogCliClient) CaptureFailedCodeownersGenerateContributorInsight() error { if p.activated { - p.client.Enqueue(posthog.Capture{ - DistinctId: "failed-codeowners-generation-contributor-insight", - Event: "cli failed to create/update contributor insight for user", + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_failed_to_create_update_contributor_insight_for_user", }) } + + return nil } -//nolint:errcheck -func (p *PosthogCliClient) CaptureInsights() { +// CaptureInsights gathers telemetry on successful Insights command runs +func (p *PosthogCliClient) CaptureInsights() error { if p.activated { - p.client.Enqueue(posthog.Capture{ - DistinctId: "insights", - Event: "cli called insights command", + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_called_insights_command", }) } + + return nil } -//nolint:errcheck -func (p *PosthogCliClient) CaptureFailedInsights() { +// CaptureFailedInsights gathers telemetry on failed Insights command runs +func (p *PosthogCliClient) CaptureFailedInsights() error { if p.activated { - p.client.Enqueue(posthog.Capture{ - DistinctId: "failed-insight", - Event: "cli failed to call insights command", + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_failed_to_call_insights_command", }) } + + return nil } diff --git a/pkg/utils/telemetry.go b/pkg/utils/telemetry.go new file mode 100644 index 0000000..ca0d82b --- /dev/null +++ b/pkg/utils/telemetry.go @@ -0,0 +1,66 @@ +package utils + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/google/uuid" +) + +var telemetryFilePath = filepath.Join(os.Getenv("HOME"), ".pizza-cli", "telemetry.json") + +// userTelemetryConfig is the config for the user's anonymous telemetry data +type userTelemetryConfig struct { + ID string `json:"id"` +} + +// getOrCreateUniqueID reads the telemetry.json file to fetch the user's anonymous, unique ID. +// In case of error (i.e., if the file doesn't exist or is invalid) it generates +// a new UUID and stores it in the telemetry.json file +func getOrCreateUniqueID() (string, error) { + if _, err := os.Stat(telemetryFilePath); os.IsNotExist(err) { + return createTelemetryUUID() + } + + data, err := os.ReadFile(telemetryFilePath) + if err != nil { + return createTelemetryUUID() + } + + // Try parsing the telemetry file + var teleData userTelemetryConfig + err = json.Unmarshal(data, &teleData) + if err != nil || teleData.ID == "" { + return createTelemetryUUID() + } + + return teleData.ID, nil +} + +// createTelemetryUUID generates a new UUID and writes it to the user's telemetry.json file +func createTelemetryUUID() (string, error) { + newUUID := uuid.New().String() + + teleData := userTelemetryConfig{ + ID: newUUID, + } + + data, err := json.Marshal(teleData) + if err != nil { + return "", fmt.Errorf("error creating telemetry data: %w", err) + } + + err = os.MkdirAll(filepath.Dir(telemetryFilePath), 0755) + if err != nil { + return "", fmt.Errorf("error creating directory for telemetry file: %w", err) + } + + err = os.WriteFile(telemetryFilePath, data, 0600) + if err != nil { + return "", fmt.Errorf("error writing telemetry file: %w", err) + } + + return newUUID, nil +} diff --git a/scripts/generate-docs.sh b/scripts/generate-docs.sh new file mode 100755 index 0000000..8b73d27 --- /dev/null +++ b/scripts/generate-docs.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status. +set -e + +# Configure git to use the OpenSauced bot account +git config user.name 'open-sauced[bot]' +git config user.email '63161813+open-sauced[bot]@users.noreply.github.com' + +# Semantic release made changes, so pull the latest changes from the current branch +git pull origin "$GITHUB_REF" + +# Generate documentation +just gen-docs + +# Get the author of the last non-merge commit +LAST_COMMIT_AUTHOR=$(git log -1 --no-merges --pretty=format:'%an <%ae>') + +# Commit with co-authorship and push changes +git add docs/ +git commit -m "chore: automated docs generation for release + +Co-authored-by: $LAST_COMMIT_AUTHOR" || echo "No changes to commit" +git push origin HEAD:"$GITHUB_REF" diff --git a/telemetry.go b/telemetry.go new file mode 100644 index 0000000..71d910a --- /dev/null +++ b/telemetry.go @@ -0,0 +1,75 @@ +//go:build telemetry +// +build telemetry + +package main + +import "github.com/open-sauced/pizza-cli/pkg/utils" + +// This alternate main is used as a one-shot for bootstrapping Posthog events: +// the various events called herein do not exist in Posthog's datalake until the +// event has landed. +// +// Therefore, this is useful for when there are new events for Posthog that need +// a dashboard bootstrapped for them. + +func main() { + println("Started bootstrapping Posthog events") + client := utils.NewPosthogCliClient(true) + + err := client.CaptureLogin("test-user") + if err != nil { + panic(err) + } + + err = client.CaptureFailedLogin() + if err != nil { + panic(err) + } + + err = client.CaptureCodeownersGenerate() + if err != nil { + panic(err) + } + + err = client.CaptureFailedCodeownersGenerate() + if err != nil { + panic(err) + } + + err = client.CaptureCodeownersGenerateAuth("test-user") + if err != nil { + panic(err) + } + + err = client.CaptureFailedCodeownersGenerateAuth() + if err != nil { + panic(err) + } + + err = client.CaptureCodeownersGenerateContributorInsight() + if err != nil { + panic(err) + } + + err = client.CaptureFailedCodeownersGenerateContributorInsight() + if err != nil { + panic(err) + } + + err = client.CaptureInsights() + if err != nil { + panic(err) + } + + err = client.CaptureFailedInsights() + if err != nil { + panic(err) + } + + err = client.Done() + if err != nil { + panic(err) + } + + println("Done bootstrapping Posthog events") +}