diff --git a/.github/workflows/pizza.yml b/.github/workflows/pizza.yml index aa0b863..852dade 100644 --- a/.github/workflows/pizza.yml +++ b/.github/workflows/pizza.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - name: Pizza Action - uses: open-sauced/pizza-action@v2.1.0 + uses: open-sauced/pizza-action@v2.3.0 with: + cli-version: "v2.2.0" commit-and-pr: "true" + pr-title: "chore: update repository codeowners" diff --git a/CHANGELOG.md b/CHANGELOG.md index 27df49a..dfa1eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ > All notable changes to this project will be documented in this file +## [2.3.0-beta.1](https://github.com/open-sauced/pizza-cli/compare/v2.2.0...v2.3.0-beta.1) (2024-10-11) + + +### 🍕 Features + +* add comment preamble to generated config ([#214](https://github.com/open-sauced/pizza-cli/issues/214)) ([deb22a2](https://github.com/open-sauced/pizza-cli/commit/deb22a247f6aa3f1a64e1ed13f6554060e67a533)) +* Add quick start section ([#203](https://github.com/open-sauced/pizza-cli/issues/203)) ([eec884b](https://github.com/open-sauced/pizza-cli/commit/eec884b816404fdbbc5fb5445cb44fe211b8b0c8)) + ## [2.2.0](https://github.com/open-sauced/pizza-cli/compare/v2.1.0...v2.2.0) (2024-09-18) @@ -12,6 +20,15 @@ * shortcut to skip rest of emails for `pizza generate config` ([#197](https://github.com/open-sauced/pizza-cli/issues/197)) ([6e96fff](https://github.com/open-sauced/pizza-cli/commit/6e96fffb3d8bec5384118f3dedc499d164384ca5)) * Upgrades to v2 packages to track with v2 git tag ([#198](https://github.com/open-sauced/pizza-cli/issues/198)) ([5a0995d](https://github.com/open-sauced/pizza-cli/commit/5a0995d8fb1405ab1d405aef0fd0f20327e2fd53)) +* add comment preamble to generated config ([#214](https://github.com/open-sauced/pizza-cli/issues/214)) ([deb22a2](https://github.com/open-sauced/pizza-cli/commit/deb22a247f6aa3f1a64e1ed13f6554060e67a533)) + +## [2.2.0-beta.3](https://github.com/open-sauced/pizza-cli/compare/v2.2.0-beta.2...v2.2.0-beta.3) (2024-09-20) + + +### 🍕 Features + +* Add quick start section ([#203](https://github.com/open-sauced/pizza-cli/issues/203)) ([eec884b](https://github.com/open-sauced/pizza-cli/commit/eec884b816404fdbbc5fb5445cb44fe211b8b0c8)) + ## [2.2.0-beta.2](https://github.com/open-sauced/pizza-cli/compare/v2.2.0-beta.1...v2.2.0-beta.2) (2024-09-18) diff --git a/CODEOWNERS b/CODEOWNERS index 55afa11..d2144a3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,7 +13,7 @@ .github/workflows/test.yaml @jpmcb @zeucapua .golangci.yaml @jpmcb @zeucapua .sauced.yaml @jpmcb @nickytonline @zeucapua -CHANGELOG.md @jpmcb @nickytonline @zeucapua +CHANGELOG.md @jpmcb @zeucapua @nickytonline CODEOWNERS @jpmcb @nickytonline Dockerfile @jpmcb @nickytonline @zeucapua Makefile @jpmcb @@ -53,7 +53,7 @@ cmd/generate/codeowners/output.go @jpmcb @zeucapua @brandonroberts cmd/generate/codeowners/output_test.go @jpmcb @brandonroberts @zeucapua cmd/generate/codeowners/spec.go @jpmcb cmd/generate/codeowners/traversal.go @jpmcb -cmd/generate/config/config.go @zeucapua @zeucapua @jpmcb +cmd/generate/config/config.go @zeucapua @jpmcb @zeucapua cmd/generate/config/output.go @zeucapua @jpmcb @zeucapua cmd/generate/config/spec.go @zeucapua @zeucapua @jpmcb cmd/generate/generate.go @jpmcb @zeucapua @@ -71,7 +71,7 @@ cmd/show/dashboard.go @jpmcb cmd/show/show.go @jpmcb cmd/show/tui.go @jpmcb cmd/version/version.go @jpmcb @nickytonline -docs/pizza.md @jpmcb @zeucapua @nickytonline +docs/pizza.md @jpmcb @nickytonline @zeucapua docs/pizza_completion.md @jpmcb @nickytonline @zeucapua docs/pizza_completion_bash.md @jpmcb @zeucapua @nickytonline docs/pizza_completion_fish.md @jpmcb @nickytonline @zeucapua @@ -80,16 +80,17 @@ docs/pizza_completion_zsh.md @jpmcb @nickytonline @zeucapua docs/pizza_generate.md @jpmcb @nickytonline @zeucapua docs/pizza_generate_codeowners.md @jpmcb docs/pizza_generate_config.md @jpmcb -docs/pizza_generate_insight.md +docs/pizza_generate_insight.md @jpmcb docs/pizza_insights.md @jpmcb @zeucapua @nickytonline -docs/pizza_insights_contributors.md @jpmcb @zeucapua @nickytonline +docs/pizza_insights_contributors.md @jpmcb @nickytonline @zeucapua docs/pizza_insights_repositories.md @jpmcb @zeucapua @nickytonline docs/pizza_insights_user-contributions.md @jpmcb @nickytonline @zeucapua docs/pizza_login.md @jpmcb @nickytonline @zeucapua docs/pizza_version.md @jpmcb @nickytonline @zeucapua go.mod @jpmcb @nickytonline @zeucapua -go.sum @jpmcb @zeucapua @zeucapua +go.sum @jpmcb @zeucapua @nickytonline justfile @jpmcb @zeucapua @nickytonline +main.go @jpmcb npm/.gitignore @jpmcb @zeucapua npm/README.md @jpmcb npm/package-lock.json @jpmcb @nickytonline @zeucapua @@ -106,5 +107,5 @@ pkg/utils/posthog.go @jpmcb @zeucapua @zeucapua pkg/utils/root.go @jpmcb @zeucapua pkg/utils/telemetry.go @jpmcb pkg/utils/version.go @nickytonline @jpmcb -scripts/generate-docs.sh @jpmcb @nickytonline +scripts/generate-docs.sh @nickytonline @jpmcb telemetry.go @jpmcb @zeucapua @zeucapua diff --git a/README.md b/README.md index 3de39d5..678c6e0 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ -# 📦 Install +# 📦 [Install](#-install) #### Homebrew @@ -149,6 +149,78 @@ jobs: how to use the Pizza command line tool and how it works with the rest of the OpenSauced ecosystem. +# 🚀 Quickstart + +Get up and running with the Pizza CLI in minutes using `npx`: + +1. **Ensure you have Node.js installed** + + Pizza CLI can be run using `npx`, which comes with Node.js. If you don't have Node.js installed, download it from [nodejs.org](https://nodejs.org/). + + > NOTE + > For other installation methods, see the [Install](#-Install) section. + +2. **Generate a configuration file** + + Navigate to your project directory and run: + ```sh + npx pizza@latest generate config ./ -i + ``` + This will create a `.sauced.yaml` file, interactively prompting you to attribute commit emails to GitHub handles. + +3. **Generate CODEOWNERS file** + + In your project directory, run: + ```sh + npx pizza@latest generate codeowners ./ + ``` + This will create a `CODEOWNERS` file based on your project's git history and the `.sauced.yaml` configuration. + +4. **Create OpenSauced [Contributor Insight](https://opensauced.pizza/docs/features/contributor-insights/)** + + After generating the CODEOWNERS file, you can create an OpenSauced Contributor Insight: + ```sh + npx pizza@latest generate insight . + ``` + +5. **Explore repository insights** + Get metrics and insights for your repository: + ```sh + npx pizza@latest insights repositories your-username/your-repo + ``` + +6. **Set up automated CODEOWNERS updates (Optional)** + Add the [Pizza GitHub Action](https://github.com/open-sauced/pizza-action) to your repository to automate CODEOWNERS updates: + ```yaml + # In .github/workflows/pizza-action.yml + name: OpenSauced Pizza Action + on: + schedule: + # Run once a week on Sunday at 00:00 UTC + - cron: "0 0 * * 0" + workflow_dispatch: # Allow manual triggering + + jobs: + pizza-action: + runs-on: ubuntu-latest + steps: + - name: Pizza Action + uses: open-sauced/pizza-action@v2.2.0 + with: + # optional and default is "latest". Add this parameter if you want to use a specific version, e.g. v2.0.0 + cli-version: "v2.2.0" + # optional and false by default. Set this to true if you want to have a pull request for the changes created automatically. + commit-and-pr: "true" + # optional + pr-title: "chore: update repository codeowners" + + ``` + +Now you're ready to leverage the Pizza CLI for managing code ownership and getting project insights with OpenSauced! + +> Note +> Using `npx pizza@latest` ensures you're always running the most recent version of Pizza CLI. If you prefer to use a specific version, you can replace `@latest` with a version number, e.g., `npx pizza@2.0.0`. + # ✨ Usage ## Codeowners generation diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go index 2743b5a..5e7d254 100644 --- a/cmd/generate/config/output.go +++ b/cmd/generate/config/output.go @@ -15,6 +15,13 @@ func generateOutputFile(outputPath string, attributionMap map[string][]string) e } defer file.Close() + // write the header preamble + _, err = file.WriteString("# Configuration for attributing commits with emails to GitHub user profiles\n# Used during codeowners generation.\n\n# List the emails associated with the given username\n# The commits associated with these emails will be attributed to\n# the username in this yaml map. Any number of emails may be listed\n\n") + + if err != nil { + return fmt.Errorf("error writing to %s file: %w", outputPath, err) + } + var config config.Spec config.Attributions = attributionMap diff --git a/cmd/offboard/offboard.go b/cmd/offboard/offboard.go new file mode 100644 index 0000000..feabfd8 --- /dev/null +++ b/cmd/offboard/offboard.go @@ -0,0 +1,139 @@ +package offboard + +import ( + "errors" + "fmt" + "slices" + "strings" + + "github.com/spf13/cobra" + + "github.com/open-sauced/pizza-cli/v2/pkg/config" + "github.com/open-sauced/pizza-cli/v2/pkg/constants" + "github.com/open-sauced/pizza-cli/v2/pkg/utils" +) + +type Options struct { + offboardingUsers []string + + // config file path + configPath string + + // repository path + path string + + // from global config + ttyDisabled bool + + // telemetry for capturing CLI events via PostHog + telemetry *utils.PosthogCliClient +} + +const offboardLongDesc string = `CAUTION: Experimental Command. Removes users from the \".sauced.yaml\" config and \"CODEOWNERS\" files. +Requires the users' name OR email.` + +func NewConfigCommand() *cobra.Command { + opts := &Options{} + cmd := &cobra.Command{ + Use: "offboard [flags]", + Short: "CAUTION: Experimental Command. Removes users from the \".sauced.yaml\" config and \"CODEOWNERS\" files.", + Long: offboardLongDesc, + Args: func(_ *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("you must provide at least one argument: the offboarding user's email/username") + } + + opts.offboardingUsers = args + + return nil + }, + RunE: func(cmd *cobra.Command, _ []string) error { + opts.ttyDisabled, _ = cmd.Flags().GetBool("tty-disable") + opts.configPath, _ = cmd.Flags().GetString("config") + disableTelem, _ := cmd.Flags().GetBool(constants.FlagNameTelemetry) + + opts.telemetry = utils.NewPosthogCliClient(!disableTelem) + + opts.path, _ = cmd.Flags().GetString("path") + err := run(opts) + _ = opts.telemetry.Done() + + return err + }, + } + + cmd.PersistentFlags().StringP("path", "p", "", "the path to the repository (required)") + if err := cmd.MarkPersistentFlagRequired("path"); err != nil { + fmt.Printf("error MarkPersistentFlagRequired: %v", err) + } + return cmd +} + +func run(opts *Options) error { + var spec *config.Spec + var err error + if len(opts.configPath) != 0 { + spec, _, err = config.LoadConfig(opts.configPath) + } else { + var configPath string + if strings.Compare(string(opts.path[len(opts.path)-1]), "/") == 0 { + configPath = opts.path + ".sauced.yaml" + } else { + configPath = opts.path + "/.sauced.yaml" + } + spec, _, err = config.LoadConfig(configPath) + } + + if err != nil { + _ = opts.telemetry.CaptureFailedOffboard() + return fmt.Errorf("error loading config: %v", err) + } + + var offboardingNames []string + attributions := spec.Attributions + for _, user := range opts.offboardingUsers { + added := false + + // deletes if the user is a name (key) + delete(attributions, user) + + // delete if the user is an email (value) + for k, v := range attributions { + if slices.Contains(v, user) { + offboardingNames = append(offboardingNames, k) + delete(attributions, k) + added = true + } + } + + if !added { + offboardingNames = append(offboardingNames, user) + } + } + + if len(opts.configPath) != 0 { + err = generateConfigFile(opts.configPath, attributions) + } else { + var configPath string + if strings.Compare(string(opts.path[len(opts.path)-1]), "/") == 0 { + configPath = opts.path + ".sauced.yaml" + } else { + configPath = opts.path + "/.sauced.yaml" + } + err = generateConfigFile(configPath, attributions) + } + + if err != nil { + _ = opts.telemetry.CaptureFailedOffboard() + return fmt.Errorf("error generating config file: %v", err) + } + + err = generateOwnersFile(opts.path, offboardingNames) + if err != nil { + _ = opts.telemetry.CaptureFailedOffboard() + return fmt.Errorf("error generating owners file: %v", err) + } + + _ = opts.telemetry.CaptureOffboard() + return nil +} diff --git a/cmd/offboard/output.go b/cmd/offboard/output.go new file mode 100644 index 0000000..f429fa0 --- /dev/null +++ b/cmd/offboard/output.go @@ -0,0 +1,87 @@ +package offboard + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/open-sauced/pizza-cli/v2/pkg/config" + "github.com/open-sauced/pizza-cli/v2/pkg/utils" +) + +func generateConfigFile(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 +} + +func generateOwnersFile(path string, offboardingUsers []string) error { + outputType := "/CODEOWNERS" + var owners []byte + var err error + + var ownersPath string + + if _, err = os.Stat(filepath.Join(path, "/CODEOWNERS")); !errors.Is(err, os.ErrNotExist) { + outputType = "CODEOWNERS" + ownersPath = filepath.Join(path, "/CODEOWNERS") + owners, err = os.ReadFile(ownersPath) + } else if _, err = os.Stat(filepath.Join(path, "OWNERS")); !errors.Is(err, os.ErrNotExist) { + outputType = "OWNERS" + ownersPath = filepath.Join(path, "/OWNERS") + owners, err = os.ReadFile(ownersPath) + } + + if err != nil { + fmt.Printf("will create a new %s file in the path %s", outputType, path) + } + + lines := strings.Split(string(owners), "\n") + var newLines []string + for _, line := range lines { + newLine := line + for _, name := range offboardingUsers { + result, _, found := strings.Cut(newLine, "@"+name) + if found { + newLine = result + } + } + newLines = append(newLines, newLine) + } + + output := strings.Join(newLines, "\n") + file, err := os.Create(ownersPath) + if err != nil { + return fmt.Errorf("error creating %s file: %w", outputType, err) + } + defer file.Close() + + _, err = file.WriteString(output) + if err != nil { + return fmt.Errorf("failed writing file %s: %w", path+outputType, err) + } + + return nil +} diff --git a/cmd/root/root.go b/cmd/root/root.go index d41a435..0cd054f 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -10,6 +10,7 @@ import ( "github.com/open-sauced/pizza-cli/v2/cmd/docs" "github.com/open-sauced/pizza-cli/v2/cmd/generate" "github.com/open-sauced/pizza-cli/v2/cmd/insights" + "github.com/open-sauced/pizza-cli/v2/cmd/offboard" "github.com/open-sauced/pizza-cli/v2/cmd/version" "github.com/open-sauced/pizza-cli/v2/pkg/constants" ) @@ -44,6 +45,7 @@ func NewRootCommand() (*cobra.Command, error) { cmd.AddCommand(generate.NewGenerateCommand()) cmd.AddCommand(insights.NewInsightsCommand()) cmd.AddCommand(version.NewVersionCommand()) + cmd.AddCommand(offboard.NewConfigCommand()) // The docs command is hidden as it's only used by the pizza-cli maintainers docsCmd := docs.NewDocsCommand() diff --git a/docs/pizza.md b/docs/pizza.md index dfd6ca4..27789df 100644 --- a/docs/pizza.md +++ b/docs/pizza.md @@ -26,5 +26,6 @@ pizza [flags] * [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 offboard](pizza_offboard.md) - CAUTION: Experimental Command. Removes users from the ".sauced.yaml" config and "CODEOWNERS" files. * [pizza version](pizza_version.md) - Displays the build version of the CLI diff --git a/docs/pizza_offboard.md b/docs/pizza_offboard.md new file mode 100644 index 0000000..82155b2 --- /dev/null +++ b/docs/pizza_offboard.md @@ -0,0 +1,33 @@ +## pizza offboard + +CAUTION: Experimental Command. Removes users from the ".sauced.yaml" config and "CODEOWNERS" files. + +### Synopsis + +CAUTION: Experimental Command. Removes users from the \".sauced.yaml\" config and \"CODEOWNERS\" files. +Requires the users' name OR email. + +``` +pizza offboard [flags] +``` + +### Options + +``` + -h, --help help for offboard + -p, --path string the path to the repository (required) +``` + +### Options inherited from parent commands + +``` + -c, --config string The codeowners config + --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](pizza.md) - OpenSauced CLI + diff --git a/npm/package-lock.json b/npm/package-lock.json index 12565e5..6aad955 100644 --- a/npm/package-lock.json +++ b/npm/package-lock.json @@ -1,12 +1,12 @@ { "name": "pizza", - "version": "2.2.0", + "version": "2.3.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pizza", - "version": "2.2.0", + "version": "2.3.0-beta.1", "hasInstallScript": true, "license": "MIT", "bin": { diff --git a/npm/package.json b/npm/package.json index 34b1c9f..876d5ea 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "pizza", - "version": "2.2.0", + "version": "2.3.0-beta.1", "description": "A command line utility for insights, metrics, and generating CODEOWNERS documentation for your open source projects", "repository": "https://github.com/open-sauced/pizza-cli", "license": "MIT", diff --git a/pkg/utils/posthog.go b/pkg/utils/posthog.go index 9a9fb41..10d66d1 100644 --- a/pkg/utils/posthog.go +++ b/pkg/utils/posthog.go @@ -198,6 +198,28 @@ func (p *PosthogCliClient) CaptureFailedConfigGenerate() error { return nil } +func (p *PosthogCliClient) CaptureOffboard() error { + if p.activated { + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_offboard", + }) + } + + return nil +} + +func (p *PosthogCliClient) CaptureFailedOffboard() error { + if p.activated { + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_failed_to_offboard", + }) + } + + return nil +} + // CaptureInsights gathers telemetry on successful Insights command runs func (p *PosthogCliClient) CaptureInsights() error { if p.activated {