diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a49980c..b61011d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -155,3 +155,6 @@ jobs: with: tags: ghcr.io/${{ github.repository }}:latest,ghcr.io/${{ github.repository }}:${{ needs.release.outputs.release-tag }} push: true + build-args: | + VERSION=${{ needs.release.outputs.release-tag }} + POSTHOG_PUBLIC_API_KEY=${{ vars.POSTHOG_WRITE_PUBLIC_KEY }} diff --git a/.sauced.yaml b/.sauced.yaml index 4726374..946586d 100644 --- a/.sauced.yaml +++ b/.sauced.yaml @@ -14,4 +14,3 @@ attribution: - nick@opensauced.pizza zeucapua: - coding@zeu.dev - diff --git a/CHANGELOG.md b/CHANGELOG.md index 661c4ff..be5e3f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,39 @@ > All notable changes to this project will be documented in this file +## [1.5.0-beta.3](https://github.com/open-sauced/pizza-cli/compare/v1.5.0-beta.2...v1.5.0-beta.3) (2024-09-16) + + +### 🍕 Features + +* Cut 2.0.0 release ([#193](https://github.com/open-sauced/pizza-cli/issues/193)) ([278a833](https://github.com/open-sauced/pizza-cli/commit/278a8339c701be1f25db9e3e039f727927b4cdef)) + +## [1.5.0-beta.2](https://github.com/open-sauced/pizza-cli/compare/v1.5.0-beta.1...v1.5.0-beta.2) (2024-09-13) + + +### 🍕 Features + +* Update README docs ([#186](https://github.com/open-sauced/pizza-cli/issues/186)) ([99328aa](https://github.com/open-sauced/pizza-cli/commit/99328aaa322b6dc4abe3f1933b6e77ffedecb355)) + +## [1.5.0-beta.1](https://github.com/open-sauced/pizza-cli/compare/v1.4.1-beta.1...v1.5.0-beta.1) (2024-09-12) + + +### 🍕 Features + +* pizza generate insight command ([#179](https://github.com/open-sauced/pizza-cli/issues/179)) ([7315a1d](https://github.com/open-sauced/pizza-cli/commit/7315a1d2ae2764dcbdfd1ce0af1272fde36b1a0c)) + + +### 🐛 Bug Fixes + +* Config path prefers local dir vs. home dir ([#184](https://github.com/open-sauced/pizza-cli/issues/184)) ([859446a](https://github.com/open-sauced/pizza-cli/commit/859446ac865e7a4c7d7779a2620c7391b4aabf33)) + +## [1.4.1-beta.1](https://github.com/open-sauced/pizza-cli/compare/v1.4.0...v1.4.1-beta.1) (2024-09-12) + + +### 🐛 Bug Fixes + +* Don't overwrite ldflags in justfile builds ([#171](https://github.com/open-sauced/pizza-cli/issues/171)) ([e024687](https://github.com/open-sauced/pizza-cli/commit/e0246879d7d72095f30305cf805ad86e2df1f623)) + ## [1.4.0](https://github.com/open-sauced/pizza-cli/compare/v1.3.0...v1.4.0) (2024-09-11) diff --git a/CODEOWNERS b/CODEOWNERS index ab3a50d..55afa11 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,101 +3,108 @@ # Generated with command: # $ pizza generate codeowners pizza-cli/ --tty-disable true -.env @jpmcb +.env @jpmcb @zeucapua .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/pizza.yml @nickytonline @jpmcb -.github/workflows/release.yaml @jpmcb @nickytonline -.github/workflows/test.yaml @jpmcb -.golangci.yaml @jpmcb -.sauced.yaml @nickytonline @jpmcb -CHANGELOG.md @jpmcb @nickytonline @brandonroberts -CODEOWNERS @nickytonline @jpmcb -Dockerfile @jpmcb @nickytonline +.github/workflows/pizza.yml @nickytonline @jpmcb @zeucapua +.github/workflows/release.yaml @jpmcb @nickytonline @zeucapua +.github/workflows/test.yaml @jpmcb @zeucapua +.golangci.yaml @jpmcb @zeucapua +.sauced.yaml @jpmcb @nickytonline @zeucapua +CHANGELOG.md @jpmcb @nickytonline @zeucapua +CODEOWNERS @jpmcb @nickytonline +Dockerfile @jpmcb @nickytonline @zeucapua Makefile @jpmcb README.md @jpmcb -api/auth/auth.go @jpmcb +api/auth/auth.go @jpmcb @zeucapua api/auth/success.html @nickytonline @jpmcb -api/client.go @jpmcb +api/client.go @jpmcb @zeucapua api/mock/mock.go @jpmcb -api/services/contributors/contributors.go @jpmcb -api/services/contributors/contributors_test.go @jpmcb +api/services/contributors/contributors.go @jpmcb @zeucapua +api/services/contributors/contributors_test.go @jpmcb @zeucapua api/services/contributors/spec.go @jpmcb -api/services/histogram/histogram.go @jpmcb -api/services/histogram/histogram_test.go @jpmcb +api/services/histogram/histogram.go @jpmcb @zeucapua +api/services/histogram/histogram_test.go @jpmcb @zeucapua api/services/histogram/spec.go @jpmcb -api/services/repository/repository.go @jpmcb -api/services/repository/repository_test.go @jpmcb +api/services/repository/repository.go @jpmcb @zeucapua +api/services/repository/repository_test.go @jpmcb @zeucapua 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/services/workspaces/spec.go @jpmcb @zeucapua +api/services/workspaces/userlists/spec.go @jpmcb @zeucapua +api/services/workspaces/userlists/userlists.go @jpmcb @zeucapua +api/services/workspaces/userlists/userlists_test.go @jpmcb @zeucapua +api/services/workspaces/workspaces.go @jpmcb @zeucapua +api/services/workspaces/workspaces_test.go @jpmcb @zeucapua api/utils/validators.go @jpmcb -cmd/auth/auth.go @jpmcb +cmd/auth/auth.go @jpmcb @zeucapua 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/docs/docs.go @nickytonline @jpmcb -cmd/docs/docs_test.go @nickytonline @jpmcb -cmd/generate/codeowners/codeowners.go @jpmcb @nickytonline @zeucapua +cmd/docs/docs.go @nickytonline @jpmcb @zeucapua +cmd/docs/docs_test.go @nickytonline @jpmcb @zeucapua +cmd/generate/codeowners/codeowners.go @jpmcb @zeucapua cmd/generate/codeowners/output.go @jpmcb @zeucapua @brandonroberts -cmd/generate/codeowners/output_test.go @jpmcb @brandonroberts +cmd/generate/codeowners/output_test.go @jpmcb @brandonroberts @zeucapua cmd/generate/codeowners/spec.go @jpmcb cmd/generate/codeowners/traversal.go @jpmcb -cmd/generate/generate.go @jpmcb -cmd/insights/contributors.go @jpmcb +cmd/generate/config/config.go @zeucapua @zeucapua @jpmcb +cmd/generate/config/output.go @zeucapua @jpmcb @zeucapua +cmd/generate/config/spec.go @zeucapua @zeucapua @jpmcb +cmd/generate/generate.go @jpmcb @zeucapua +cmd/generate/insight/insight.go @jpmcb +cmd/insights/contributors.go @jpmcb @zeucapua cmd/insights/insights.go @jpmcb @brandonroberts -cmd/insights/repositories.go @jpmcb -cmd/insights/user-contributions.go @jpmcb +cmd/insights/repositories.go @jpmcb @zeucapua +cmd/insights/user-contributions.go @jpmcb @zeucapua cmd/insights/utils.go @jpmcb cmd/repo-query/repo-query.go @jpmcb -cmd/root/root.go @jpmcb @brandonroberts @nickytonline +cmd/root/root.go @jpmcb @brandonroberts @zeucapua 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 -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/.gitignore @jpmcb -npm/package-lock.json @jpmcb @nickytonline @brandonroberts +docs/pizza.md @jpmcb @zeucapua @nickytonline +docs/pizza_completion.md @jpmcb @nickytonline @zeucapua +docs/pizza_completion_bash.md @jpmcb @zeucapua @nickytonline +docs/pizza_completion_fish.md @jpmcb @nickytonline @zeucapua +docs/pizza_completion_powershell.md @jpmcb @nickytonline @zeucapua +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_insights.md @jpmcb @zeucapua @nickytonline +docs/pizza_insights_contributors.md @jpmcb @zeucapua @nickytonline +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 +justfile @jpmcb @zeucapua @nickytonline +npm/.gitignore @jpmcb @zeucapua +npm/README.md @jpmcb +npm/package-lock.json @jpmcb @nickytonline @zeucapua 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/config.go @jpmcb @nickytonline @zeucapua +pkg/config/config_test.go @nickytonline @jpmcb @zeucapua pkg/config/file.go @jpmcb -pkg/config/spec.go @jpmcb @brandonroberts +pkg/config/spec.go @jpmcb @brandonroberts @zeucapua pkg/constants/flags.go @jpmcb pkg/logging/constants.go @jpmcb -pkg/utils/posthog.go @jpmcb -pkg/utils/root.go @jpmcb +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 @nickytonline -telemetry.go @jpmcb +scripts/generate-docs.sh @jpmcb @nickytonline +telemetry.go @jpmcb @zeucapua @zeucapua diff --git a/README.md b/README.md index 9f4b80c..b590634 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,35 @@

- Open Sauced

🍕 Pizza CLI 🍕

- A command line interface and tool for all things OpenSauced! + A Go command line interface for managing code ownership and project insights with OpenSauced!
+
+ Watch the overview video 👇 +
+
+ + CODEOWNERS demo + +

+
+ GitHub code size in bytes + + GitHub issues + + + GitHub Release + + + Twitter + + + + +
+ # 📦 Install #### Homebrew @@ -21,29 +44,13 @@ brew install open-sauced/tap/pizza npm i -g pizza ``` -### Docker +You can also use `npx` to run one-off commands without installing anything: ```sh -$ docker run ghcr.io/open-sauced/pizza-cli:latest -``` - -For commands that require access to your file system (like `generate codeowners`), ensure -you pass a volume to the docker container: - -```sh -$ docker run -v /local/path:/container/path ghcr.io/open-sauced/pizza-cli:latest \ - generate codeowners /container/path -``` - -For example, to mount your entire home directory (which may include a `.sauced.yaml` file -alongside the project you want to generate a `CODEOWNERS` file for): - -```sh -$ docker run -v ~/:/app ghcr.io/open-sauced/pizza-cli:latest \ - codeowners /app/workspace/gopherlogs -c /app/.sauced.yaml +npx pizza@latest generate codeowners . ``` -### Go install +#### Go install Using the Go tool-chain, you can install the binary directly: @@ -51,10 +58,11 @@ Using the Go tool-chain, you can install the binary directly: $ go install github.com/open-sauced/pizza-cli@latest ``` -Warning! You should have the `GOBIN` env var setup to point to a persistent -location in your `PATH`. After Go 1.16, this defaults to `GOPATH[0]/bin`. +> [!WARNING] +> Warning! You should have the `GOBIN` env var setup to point to a persistent +> location in your `PATH`. After Go 1.16, this defaults to `GOPATH[0]/bin`. -### Manual install +#### Manual install Download a pre-built artifact from [the GitHub releases](https://github.com/open-sauced/pizza-cli/releases): @@ -67,7 +75,7 @@ $ chmod +x ~/Downloads/pizza-linux-arm64 $ mv ~/Downloads/pizza-linux-arm64 /usr/local/share/bin/pizza ``` -#### Direct script install +#### Script install ```sh curl -fsSL https://raw.githubusercontent.com/open-sauced/pizza-cli/main/install.sh | sh @@ -91,88 +99,210 @@ your system's `$PATH`. > ./install.sh > ``` -#### Manual build +# 🐳 Docker -Clone this repository. Then, using the Go tool-chain, you can build a binary: +Use the container image of the CLI for use in CI/CD or automation: +```sh +$ docker run ghcr.io/open-sauced/pizza-cli:latest ``` -$ go build -o build/pizza main.go + +For commands that require access to your file system (like `generate codeowners`), ensure +you pass a volume to the docker container: + +```sh +$ docker run -v /local/path:/container/path ghcr.io/open-sauced/pizza-cli:latest \ + generate codeowners /container/path +``` + +For example, to mount your entire home directory (which may include a `~/.sauced.yaml` file +alongside the project you want to generate a `CODEOWNERS` file for): + +```sh +$ docker run -v ~/:/app ghcr.io/open-sauced/pizza-cli:latest \ + codeowners /app/workspace/gopherlogs -c /app/.sauced.yaml +``` + +# 🍕 Pizza Action + +Use [the Pizza GitHub Action](https://github.com/open-sauced/pizza-action) for running `pizza` operations in GitHub CI/CD, +like automated `CODEOWNERS` updating and pruning: + +```yaml +jobs: + pizza-action: + runs-on: ubuntu-latest + steps: + - name: Pizza Action + uses: open-sauced/pizza-action@v2 + with: + # Optional: Whether to commit and create a PR for "CODEOWNER" changes + commit-and-pr: "true" + # Optional: Title of the PR for review by team + pr-title: "chore: update repository codeowners" ``` -Warning! There may be unsupported features, breaking changes, or experimental -patches on the tip of the repository. Go and build with caution! +# 📝 Docs + +- [Pizza.md](./docs/pizza.md): In depth docs on each command, option, and flag. +- [OpenSauced.pizza/docs](https://opensauced.pizza/docs/tools/pizza-cli/): Learn + how to use the Pizza command line tool and how it works with the rest of the OpenSauced + ecosystem. # ✨ Usage -### Codeowners generation +## Codeowners generation -Use the `codeowners` command to generate an `OWNERS` file or GitHub style `CODEOWNERS` file. +Use the `codeowners` command to generate a GitHub style `CODEOWNERS` file or a more agnostic `OWNERS` file. This can be used to granularly define what experts and entities have the most context and knowledge on certain parts of a codebase. +It's expected that there's a `.sauced.yaml` config file in the given path or in +your home directory (as `~/.sauced.yaml`): + +```sh +pizza generate codeowners /path/to/local/git/repo ``` -❯ pizza generate codeowners -h -WARNING: Proof of concept feature. +Running this command will iterate the git ref-log to determine who to set as a code +owner based on the number of lines changed for that file within the given time range. +The first owner is the entity with the most lines changed. This command uses a `.sauced.yaml` configuration +to attribute emails in commits with the given entities in the config (like GitHub usernames or teams). +See [the section on the configuration schema for more details](#-configuration-schema) + +### 🚀 New in v1.4.0: Generate Config + +The `pizza generate config` command has been added to help you create `.sauced.yaml` configuration files for your projects. +This command allows you to generate configuration files with various options: + +```sh +pizza generate config /path/to/local/git/repo +``` -Generates a CODEOWNERS file for a given git repository. This uses a ~/.sauced.yaml -configuration to attribute emails with given entities. +This command will iterate the git ref-log and inspect email signatures for commits +and, in interactive mode, ask you to attribute those users with GitHub handles. Once finished, the resulting +`.sauced.yaml` file can be used to attribute owners in a `CODEOWNERS` file during `pizza generate codeowners`. + +#### Flags: + +- `-i, --interactive`: Enter interactive mode to attribute each email manually +- `-o, --output-path string`: Set the directory for the output file +- `-h, --help`: Display help for the command + +#### Examples: + +1. Generate a config file in the current directory: + ```sh + pizza generate config ./ + ``` + +2. Generate a config file interactively: + ```sh + pizza generate config ./ -i + ``` + +3. Generate a config file from the current directory and place resulting `.sauced.yaml` in a specific output directory: + ```sh + pizza generate config ./ -o /path/to/directory + ``` + +## OpenSauced Contributor Insight from `CODEOWNERS` + +You can create an [OpenSauced Contributor Insight](https://opensauced.pizza/docs/features/contributor-insights/) +from a local `CODEOWNERS` file: + +``` +pizza generate insight /path/to/repo/with/CODEOWNERS/file +``` -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. +This will parse the `CODEOWNERS` file and create a Contributor Insight on the OpenSauced platform. +This allows you to track insights and metrics for those codeowners, powered by OpenSauced. -Usage: - pizza generate codeowners path/to/repo [flags] +## Insights -Flags: - --owners-style-file Whether to generate an agnostic OWNERS style file. - -h, --help help for codeowners - -r, --range int The number of days to lookback (default 90) +You can get metrics and insights on repositories, contributors, and more: -Global Flags: - --beta Shorthand for using the beta OpenSauced API endpoint ("https://beta.api.opensauced.pizza"). - Supersedes the '--endpoint' flag - -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") - -o, --output string The formatting for command output. One of: (table, yaml, csv, json) (default "table") - --tty-disable Disable log stylization. Suitable for CI/CD and automation ``` +pizza insights [sub-command] +``` + +This powerful command lets you compose many metrics and insights together, all +powered by OpenSauced's API. Use the `--output` flag to output the results as yaml, json, csv, etc. -### Configuration +# 🎷 Configuration schema ```yaml # Configuration for attributing commits with emails to individual entities. # Used during "pizza generate codeowners". attribution: - # Keys can be GitHub usernames. For the "--github-codeowners" style codeowners - # generation, these keys must be valid GitHub usernames. + # Keys can be GitHub usernames. jpmcb: - # List of emails associated with the given entity. + # List of emails associated with the given GitHub login. # The commits associated with these emails will be attributed to - # the entity in this yaml map. Any number of emails may be listed. + # this GitHub login in this yaml map. Any number of emails may be listed. - john@opensauced.pizza - hello@johncodes.com - # Entities may also be GitHub teams. + # Keys may also be GitHub teams. This is useful for orchestrating multiple + # people to a sole GitHub team. open-sauced/engineering: - john@opensauced.pizza - other-user@email.com - other-user@no-reply.github.com - # They can also be agnostic names which will land as keys in OWNERS files + # Keys can also be agnostic names which will land as keys in "OWNERS" files + # when the "--owners-style-file" flag is set. John McBride - john@opensauced.pizza + +# Used during codeowners generation: if there are no code owners found +# for a file within the time range, the list of fallback entities +# will be used +attribution-fallback: + - open-sauced/engineering + - some-other-github-login ``` # 🚜 Development -### 🔨 Requirements +### Requirements There are a few things you'll need to get started: - The [1.22 `go` programming language](https://go.dev/doc/install) toolchain and dev environment (for example, the [VScode Go plugin](https://code.visualstudio.com/docs/languages/go) is very good). - The [`just` command runner](https://github.com/casey/just) for easy operations + +### Building + +Clone this repository. Then, using the Go tool-chain, you can build a binary: + +``` +go build -o build/pizza main.go +``` + +> [!WARNING] +> There may be unsupported features, breaking changes, or experimental +> patches on the tip of the repository. Go and build with caution! + +There are a number of `just` convinence commands for building with injected buildtime variables +and targeting other architectures and operating systems. + +``` +just build +``` +``` +just build-all +``` + +### Dev operations + +There are a number of useful `just` commands that should be used during development: +- `just lint` will us Golangci-lint to lint the Go code +- `just clean` removes build artifacts from `build/` +- `just test` runs the unit and e2e tests +- `just format` uses goimports to format code +- ... and many more! + +Check `just help` to get a full list of utility dev commands! diff --git a/cmd/generate/codeowners/codeowners.go b/cmd/generate/codeowners/codeowners.go index 20491dd..f2ad82b 100644 --- a/cmd/generate/codeowners/codeowners.go +++ b/cmd/generate/codeowners/codeowners.go @@ -11,10 +11,6 @@ import ( "github.com/jpmcb/gopherlogs/pkg/colors" "github.com/spf13/cobra" - "github.com/open-sauced/pizza-cli/api" - "github.com/open-sauced/pizza-cli/api/auth" - "github.com/open-sauced/pizza-cli/api/services/workspaces" - "github.com/open-sauced/pizza-cli/api/services/workspaces/userlists" "github.com/open-sauced/pizza-cli/pkg/config" "github.com/open-sauced/pizza-cli/pkg/constants" "github.com/open-sauced/pizza-cli/pkg/logging" @@ -33,9 +29,6 @@ type Options struct { // the number of days to look back previousDays int - // the session token adding codeowners to a workspace contributor list - token string - logger gopherlogs.Logger tty bool loglevel int @@ -43,7 +36,8 @@ type Options struct { // telemetry for capturing CLI events via PostHog telemetry *utils.PosthogCliClient - config *config.Spec + config *config.Spec + configLoadedPath string } 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. @@ -109,7 +103,11 @@ pizza generate codeowners . --config /path/to/.sauced.yaml opts.telemetry = utils.NewPosthogCliClient(!disableTelem) configPath, _ := cmd.Flags().GetString("config") - opts.config, err = config.LoadConfig(configPath) + if configPath == "" { + configPath = filepath.Join(opts.path, ".sauced.yaml") + } + + opts.config, opts.configLoadedPath, err = config.LoadConfig(configPath) if err != nil { return err } @@ -155,6 +153,7 @@ func run(opts *Options, cmd *cobra.Command) error { return fmt.Errorf("could not build logger: %w", err) } opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Built logger with log level: %d\n", opts.loglevel) + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Loaded config from: %s\n", opts.configLoadedPath) repo, err := git.PlainOpen(opts.path) if err != nil { @@ -194,188 +193,9 @@ func run(opts *Options, cmd *cobra.Command) error { opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Finished generating file: %s\n", outputPath) _ = 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 - fmt.Print("Do you want to add these codeowners to an OpenSauced Contributor Insight? (y/n): ") - _, err = fmt.Scanln(&input) - if err != nil { - return fmt.Errorf("could not scan input from terminal: %w", err) - } - - switch input { - case "y", "Y", "yes": - opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Adding codeowners to contributor insight\n") - case "n", "N", "no": - return nil - default: - return errors.New("invalid answer. Please enter y or n") - } - - // 2. Check if user is logged in. Log them in if not. - opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Initiating log in flow\n") - authenticator := auth.NewAuthenticator() - err = authenticator.CheckSession() - if err != nil { - opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Log in session invalid: %s\n", err) - fmt.Print("Do you want to log into OpenSauced? (y/n): ") - _, err := fmt.Scanln(&input) - if err != nil { - return fmt.Errorf("could not scan input from terminal: %w", err) - } - - switch input { - case "y", "Y", "yes": - user, err := authenticator.Login() - if err != nil { - _ = 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.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Logged in as: %s\n", user) - - case "n", "N", "no": - return nil - - default: - return errors.New("invalid answer. Please enter y or n") - } - } - - opts.token, err = authenticator.GetSessionToken() - if err != nil { - _ = 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) - } - - listName := filepath.Base(opts.path) - - 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.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) - } - opts.logger.V(logging.LogDebug).Style(0, colors.FgGreen).Infof("Found workspace: Pizza CLI\n") - - 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.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.logger.V(logging.LogInfo).Style(0, colors.FgCyan).Infof("\nCreate an OpenSauced Contributor Insight to get metrics and insights on these codeowners:\n") + opts.logger.V(logging.LogInfo).Style(0, colors.FgCyan).Infof("$ pizza generate insight " + opts.path + "\n") _ = opts.telemetry.CaptureCodeownersGenerateContributorInsight() return nil } - -// findCreatePizzaCliWorkspace finds or creates a "Pizza CLI" workspace -// for the authenticated user -func findCreatePizzaCliWorkspace(opts *Options) (*workspaces.DbWorkspace, error) { - nextPage := true - page := 1 - apiClient := api.NewClient("https://api.opensauced.pizza") - - for nextPage { - opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Query user workspaces page: %d\n", page) - workspaceResp, _, err := apiClient.WorkspacesService.GetWorkspaces(opts.token, page, 100) - if err != nil { - return nil, err - } - - for _, workspace := range workspaceResp.Data { - if workspace.Name == "Pizza CLI" { - opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Found existing workspace named: Pizza CLI\n") - return &workspace, nil - } - } - - nextPage = workspaceResp.Meta.HasNextPage - page++ - } - - opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Creating new user workspace: Pizza CLI\n") - newWorkspace, _, err := apiClient.WorkspacesService.CreateWorkspaceForUser(opts.token, "Pizza CLI", "A workspace for the Pizza CLI", []string{}) - if err != nil { - return nil, err - } - - return newWorkspace, nil -} - -// updateCreateLocalWorkspaceUserList updates or creates a workspace contributor list -// for the authenticated user with the given codeowners -func updateCreateLocalWorkspaceUserList(opts *Options, listName string, workspace *workspaces.DbWorkspace, codeowners FileStats) (*userlists.DbUserList, error) { - nextPage := true - page := 1 - apiClient := api.NewClient("https://api.opensauced.pizza") - - var targetUserListID string - - for nextPage { - opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Query user Workspace Contributor Insight page: %d\n", page) - userListsResp, _, err := apiClient.WorkspacesService.UserListService.GetUserLists(opts.token, workspace.ID, page, 100) - if err != nil { - return nil, err - } - - nextPage = userListsResp.Meta.HasNextPage - page++ - - for _, userList := range userListsResp.Data { - if userList.Name == listName { - opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Found existing Workspace Contributor Insight named: %s\n", listName) - targetUserListID = userList.ID - nextPage = false - } - } - } - - if targetUserListID == "" { - var err error - - opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Creating new user Workspace Contributor List: %s\n", listName) - createdUserList, _, err := apiClient.WorkspacesService.UserListService.CreateUserListForUser(opts.token, workspace.ID, listName, []string{}) - if err != nil { - return nil, err - } - - targetUserListID = createdUserList.UserListID - } - - targetUserList, _, err := apiClient.WorkspacesService.UserListService.GetUserList(opts.token, workspace.ID, targetUserListID) - if err != nil { - return nil, err - } - - // create a mapping of author logins to empty structs (i.e., a unique set). - // this de-structures the { filename: author-stats } mapping that originally - // built the codeowners - uniqueLogins := make(map[string]struct{}) - for _, codeowner := range codeowners { - for _, k := range codeowner { - if k.GitHubAlias != "" { - uniqueLogins[k.GitHubAlias] = struct{}{} - } - } - } - - logins := []string{} - for login := range uniqueLogins { - logins = append(logins, login) - } - - opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Updating Contributor Insight with codeowners with GitHub aliases: %v\n", logins) - userlist, _, err := apiClient.WorkspacesService.UserListService.PatchUserListForUser(opts.token, workspace.ID, targetUserList.ID, targetUserList.Name, logins) - return userlist, err -} diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index e5e3e19..bc39df4 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -11,6 +11,9 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "github.com/spf13/cobra" + + "github.com/open-sauced/pizza-cli/pkg/constants" + "github.com/open-sauced/pizza-cli/pkg/utils" ) // Options for the config generation command @@ -26,10 +29,15 @@ type Options struct { // from global config ttyDisabled bool + + // telemetry for capturing CLI events via PostHog + telemetry *utils.PosthogCliClient } -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.` +const configLongDesc string = `Generates a ".sauced.yaml" configuration file for use with the Pizza CLI's codeowners command. + +This command analyzes the git history of the current repository to create a mapping +of email addresses to GitHub usernames. ` func NewConfigCommand() *cobra.Command { opts := &Options{} @@ -60,11 +68,17 @@ func NewConfigCommand() *cobra.Command { }, RunE: func(cmd *cobra.Command, _ []string) error { + disableTelem, _ := cmd.Flags().GetBool(constants.FlagNameTelemetry) + + opts.telemetry = utils.NewPosthogCliClient(!disableTelem) opts.outputPath, _ = cmd.Flags().GetString("output-path") opts.isInteractive, _ = cmd.Flags().GetBool("interactive") opts.ttyDisabled, _ = cmd.Flags().GetBool("tty-disable") - return run(opts) + err := run(opts) + _ = opts.telemetry.Done() + + return err }, } @@ -79,12 +93,14 @@ func run(opts *Options) error { // Open repo repo, err := git.PlainOpen(opts.path) if err != nil { + _ = opts.telemetry.CaptureFailedConfigGenerate() return fmt.Errorf("error opening repo: %w", err) } commitIter, err := repo.CommitObjects() if err != nil { + _ = opts.telemetry.CaptureFailedConfigGenerate() return fmt.Errorf("error opening repo commits: %w", err) } @@ -111,11 +127,14 @@ func run(opts *Options) error { // INTERACTIVE: per unique email, set a name (existing or new or ignore) if opts.isInteractive && !opts.ttyDisabled { + _ = opts.telemetry.CaptureConfigGenerateMode("interactive") program := tea.NewProgram(initialModel(opts, uniqueEmails)) if _, err := program.Run(); err != nil { + _ = opts.telemetry.CaptureFailedConfigGenerate() return fmt.Errorf("error running interactive mode: %w", err) } } else { + _ = opts.telemetry.CaptureConfigGenerateMode("automatic") // generate an output file // default: `./.sauced.yaml` // fallback for home directories @@ -123,15 +142,18 @@ func run(opts *Options) error { homeDir, _ := os.UserHomeDir() err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) if err != nil { + _ = opts.telemetry.CaptureFailedConfigGenerate() return fmt.Errorf("error generating output file: %w", err) } } else { err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) if err != nil { + _ = opts.telemetry.CaptureFailedConfigGenerate() return fmt.Errorf("error generating output file: %w", err) } } } + _ = opts.telemetry.CaptureConfigGenerate() return nil } diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go index 13d7396..47edbd0 100644 --- a/cmd/generate/generate.go +++ b/cmd/generate/generate.go @@ -7,6 +7,7 @@ import ( "github.com/open-sauced/pizza-cli/cmd/generate/codeowners" "github.com/open-sauced/pizza-cli/cmd/generate/config" + "github.com/open-sauced/pizza-cli/cmd/generate/insight" ) const generateLongDesc string = `The 'generate' command provides tools to automate the creation of important project documentation and derive insights from your codebase.` @@ -28,6 +29,7 @@ func NewGenerateCommand() *cobra.Command { cmd.AddCommand(codeowners.NewCodeownersCommand()) cmd.AddCommand(config.NewConfigCommand()) + cmd.AddCommand(insight.NewGenerateInsightCommand()) return cmd } diff --git a/cmd/generate/insight/insight.go b/cmd/generate/insight/insight.go new file mode 100644 index 0000000..5769316 --- /dev/null +++ b/cmd/generate/insight/insight.go @@ -0,0 +1,332 @@ +package insight + +import ( + "bufio" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + + "github.com/jpmcb/gopherlogs" + "github.com/jpmcb/gopherlogs/pkg/colors" + "github.com/spf13/cobra" + + "github.com/open-sauced/pizza-cli/api" + "github.com/open-sauced/pizza-cli/api/auth" + "github.com/open-sauced/pizza-cli/api/services/workspaces" + "github.com/open-sauced/pizza-cli/api/services/workspaces/userlists" + "github.com/open-sauced/pizza-cli/pkg/constants" + "github.com/open-sauced/pizza-cli/pkg/logging" + "github.com/open-sauced/pizza-cli/pkg/utils" +) + +// Options for the codeowners generation command +type Options struct { + // the path to the git repository on disk to generate a codeowners file for + path string + + logger gopherlogs.Logger + tty bool + loglevel int + + token string + + // telemetry for capturing CLI events via PostHog + telemetry *utils.PosthogCliClient +} + +const insightLongDesc string = `Generate an OpenSauced Contributor Insight based on GitHub logins in a CODEOWNERS file +to get metrics and insights on those users. + +The provided path must be a local git repo with a valid CODEOWNERS file and GitHub "@login" +for each codeowner. + +After logging in, the generated Contributor Insight on OpenSauced will have insights on +active contributors, contributon velocity, and more.` + +const insightExamples string = ` # Use CODEOWNERS file in explicit directory + $ pizza generate insight /path/to/repo + + # Use CODEOWNERS file in local directory + $ pizza generate insight .` + +func NewGenerateInsightCommand() *cobra.Command { + opts := &Options{} + + cmd := &cobra.Command{ + Use: "insight path/to/repo/with/CODEOWNERS/file [flags]", + Short: "Generate an OpenSauced Contributor Insight based on GitHub logins in a CODEOWNERS file", + Long: insightLongDesc, + Example: insightExamples, + Args: func(_ *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("you must provide exactly one argument: the path to a repository with a codeowners file") + } + + 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 { + var err error + + disableTelem, _ := cmd.Flags().GetBool(constants.FlagNameTelemetry) + + opts.telemetry = utils.NewPosthogCliClient(!disableTelem) + opts.tty, _ = cmd.Flags().GetBool("tty-disable") + + loglevelS, _ := cmd.Flags().GetString("log-level") + + switch loglevelS { + case "error": + opts.loglevel = logging.LogError + case "warn": + opts.loglevel = logging.LogWarn + case "info": + opts.loglevel = logging.LogInfo + case "debug": + opts.loglevel = logging.LogDebug + } + + err = run(opts, cmd) + + _ = opts.telemetry.Done() + + return err + }, + } + + return cmd +} + +func run(opts *Options, _ *cobra.Command) error { + var err error + opts.logger, err = gopherlogs.NewLogger( + gopherlogs.WithLogVerbosity(opts.loglevel), + gopherlogs.WithTty(!opts.tty), + ) + if err != nil { + return fmt.Errorf("could not build logger: %w", err) + } + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Built logger with log level: %d\n", opts.loglevel) + + codeowners, err := getUniqueCodeowners(filepath.Join(opts.path, "CODEOWNERS")) + if err != nil { + return fmt.Errorf("could not get codeowners from file: %s - %w", opts.path, err) + } + + // 1. Ask if they want to add users to a list + var input string + fmt.Print("Do you want to add these codeowners to an OpenSauced Contributor Insight? (y/n): ") + _, err = fmt.Scanln(&input) + if err != nil { + return fmt.Errorf("could not scan input from terminal: %w", err) + } + + switch input { + case "y", "Y", "yes": + opts.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Adding codeowners to Contributor Insight\n") + case "n", "N", "no": + return nil + default: + return errors.New("invalid answer. Please enter y or n") + } + + // 2. Check if user is logged in. Log them in if not. + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Initiating log in flow\n") + authenticator := auth.NewAuthenticator() + err = authenticator.CheckSession() + if err != nil { + opts.logger.V(logging.LogInfo).Style(0, colors.FgRed).Infof("Log in session invalid: %s\n", err) + fmt.Print("Do you want to log into OpenSauced? (y/n): ") + _, err := fmt.Scanln(&input) + if err != nil { + return fmt.Errorf("could not scan input from terminal: %w", err) + } + + switch input { + case "y", "Y", "yes": + user, err := authenticator.Login() + if err != nil { + _ = 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.logger.V(logging.LogInfo).Style(0, colors.FgGreen).Infof("Logged in as: %s\n", user) + + case "n", "N", "no": + return nil + + default: + return errors.New("invalid answer. Please enter y or n") + } + } + + opts.token, err = authenticator.GetSessionToken() + if err != nil { + _ = 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) + } + + listName := filepath.Base(opts.path) + + 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.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) + } + opts.logger.V(logging.LogDebug).Style(0, colors.FgGreen).Infof("Found workspace: Pizza CLI\n") + + 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.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("\nAccess Contributor Insight on OpenSauced:\n%s\n", fmt.Sprintf("https://app.opensauced.pizza/workspaces/%s/contributor-insights/%s", workspace.ID, userList.ID)) + _ = opts.telemetry.CaptureCodeownersGenerateContributorInsight() + + return nil +} + +func getUniqueCodeowners(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("error opening file: %w", err) + } + defer file.Close() + + // Create a map to store unique uniqueLogins + uniqueLogins := make(map[string]struct{}) + + // Create a regular expression to match GitHub usernames + re := regexp.MustCompile(`@(\w+)`) + + // Read the file line by line + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + + // Find all matches in the line + matches := re.FindAllStringSubmatch(line, -1) + + // Add each match to the map + for _, match := range matches { + uniqueLogins[match[1]] = struct{}{} + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + + logins := []string{} + for login := range uniqueLogins { + logins = append(logins, login) + } + + fmt.Printf("%v\n", logins) + return logins, nil +} + +// findCreatePizzaCliWorkspace finds or creates a "Pizza CLI" workspace +// for the authenticated user +func findCreatePizzaCliWorkspace(opts *Options) (*workspaces.DbWorkspace, error) { + nextPage := true + page := 1 + apiClient := api.NewClient("https://api.opensauced.pizza") + + for nextPage { + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Query user workspaces page: %d\n", page) + workspaceResp, _, err := apiClient.WorkspacesService.GetWorkspaces(opts.token, page, 100) + if err != nil { + return nil, err + } + + for _, workspace := range workspaceResp.Data { + if workspace.Name == "Pizza CLI" { + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Found existing workspace named: Pizza CLI\n") + return &workspace, nil + } + } + + nextPage = workspaceResp.Meta.HasNextPage + page++ + } + + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Creating new user workspace: Pizza CLI\n") + newWorkspace, _, err := apiClient.WorkspacesService.CreateWorkspaceForUser(opts.token, "Pizza CLI", "A workspace for the Pizza CLI", []string{}) + if err != nil { + return nil, err + } + + return newWorkspace, nil +} + +// updateCreateLocalWorkspaceUserList updates or creates a workspace contributor list +// for the authenticated user with the given codeowners +func updateCreateLocalWorkspaceUserList(opts *Options, listName string, workspace *workspaces.DbWorkspace, logins []string) (*userlists.DbUserList, error) { + nextPage := true + page := 1 + apiClient := api.NewClient("https://api.opensauced.pizza") + + var targetUserListID string + + for nextPage { + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Query user Workspace Contributor Insight page: %d\n", page) + userListsResp, _, err := apiClient.WorkspacesService.UserListService.GetUserLists(opts.token, workspace.ID, page, 100) + if err != nil { + return nil, err + } + + nextPage = userListsResp.Meta.HasNextPage + page++ + + for _, userList := range userListsResp.Data { + if userList.Name == listName { + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Found existing Workspace Contributor Insight named: %s\n", listName) + targetUserListID = userList.ID + nextPage = false + } + } + } + + if targetUserListID == "" { + var err error + + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Creating new user Workspace Contributor List: %s\n", listName) + createdUserList, _, err := apiClient.WorkspacesService.UserListService.CreateUserListForUser(opts.token, workspace.ID, listName, []string{}) + if err != nil { + return nil, err + } + + targetUserListID = createdUserList.UserListID + } + + targetUserList, _, err := apiClient.WorkspacesService.UserListService.GetUserList(opts.token, workspace.ID, targetUserListID) + if err != nil { + return nil, err + } + + opts.logger.V(logging.LogDebug).Style(0, colors.FgBlue).Infof("Updating Contributor Insight with codeowners with GitHub aliases: %v\n", logins) + userlist, _, err := apiClient.WorkspacesService.UserListService.PatchUserListForUser(opts.token, workspace.ID, targetUserList.ID, targetUserList.Name, logins) + return userlist, err +} diff --git a/cmd/root/root.go b/cmd/root/root.go index 7b35e4a..bbf81ae 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -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 codeowners config") + cmd.PersistentFlags().StringP("config", "c", "", "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 58ea659..dfd6ca4 100644 --- a/docs/pizza.md +++ b/docs/pizza.md @@ -13,7 +13,7 @@ pizza [flags] ### Options ``` - -c, --config string The codeowners config (default "~/.sauced.yaml") + -c, --config string The codeowners config --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") diff --git a/docs/pizza_completion.md b/docs/pizza_completion.md index 1506e1b..c3a9471 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 codeowners config (default "~/.sauced.yaml") + -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 diff --git a/docs/pizza_completion_bash.md b/docs/pizza_completion_bash.md index 3a32c97..ea8aae4 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 codeowners config (default "~/.sauced.yaml") + -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 diff --git a/docs/pizza_completion_fish.md b/docs/pizza_completion_fish.md index 734d1e0..a8d278a 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 codeowners config (default "~/.sauced.yaml") + -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 diff --git a/docs/pizza_completion_powershell.md b/docs/pizza_completion_powershell.md index 06b2036..4de607a 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 codeowners config (default "~/.sauced.yaml") + -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 diff --git a/docs/pizza_completion_zsh.md b/docs/pizza_completion_zsh.md index 5e45162..ccc8522 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 codeowners config (default "~/.sauced.yaml") + -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 diff --git a/docs/pizza_generate.md b/docs/pizza_generate.md index 61b0e8e..49c37ca 100644 --- a/docs/pizza_generate.md +++ b/docs/pizza_generate.md @@ -19,7 +19,7 @@ pizza generate [subcommand] [flags] ### Options inherited from parent commands ``` - -c, --config string The codeowners config (default "~/.sauced.yaml") + -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 @@ -30,4 +30,5 @@ pizza generate [subcommand] [flags] * [pizza](pizza.md) - OpenSauced CLI * [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 +* [pizza generate insight](pizza_generate_insight.md) - Generate an OpenSauced Contributor Insight based on GitHub logins in a CODEOWNERS file diff --git a/docs/pizza_generate_codeowners.md b/docs/pizza_generate_codeowners.md index dcbbfd0..fc69c5e 100644 --- a/docs/pizza_generate_codeowners.md +++ b/docs/pizza_generate_codeowners.md @@ -52,7 +52,7 @@ pizza generate codeowners . --config /path/to/.sauced.yaml ### Options inherited from parent commands ``` - -c, --config string The codeowners config (default "~/.sauced.yaml") + -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 diff --git a/docs/pizza_generate_config.md b/docs/pizza_generate_config.md index 0366cad..bc89a18 100644 --- a/docs/pizza_generate_config.md +++ b/docs/pizza_generate_config.md @@ -4,8 +4,10 @@ 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. +Generates a ".sauced.yaml" configuration file for use with the Pizza CLI's codeowners command. + +This command analyzes the git history of the current repository to create a mapping +of email addresses to GitHub usernames. ``` pizza generate config path/to/repo [flags] @@ -22,7 +24,7 @@ pizza generate config path/to/repo [flags] ### Options inherited from parent commands ``` - -c, --config string The codeowners config (default "~/.sauced.yaml") + -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 diff --git a/docs/pizza_generate_insight.md b/docs/pizza_generate_insight.md new file mode 100644 index 0000000..628c1a3 --- /dev/null +++ b/docs/pizza_generate_insight.md @@ -0,0 +1,48 @@ +## pizza generate insight + +Generate an OpenSauced Contributor Insight based on GitHub logins in a CODEOWNERS file + +### Synopsis + +Generate an OpenSauced Contributor Insight based on GitHub logins in a CODEOWNERS file +to get metrics and insights on those users. + +The provided path must be a local git repo with a valid CODEOWNERS file and GitHub "@login" +for each codeowner. + +After logging in, the generated Contributor Insight on OpenSauced will have insights on +active contributors, contributon velocity, and more. + +``` +pizza generate insight path/to/repo/with/CODEOWNERS/file [flags] +``` + +### Examples + +``` + # Use CODEOWNERS file in explicit directory + $ pizza generate insight /path/to/repo + + # Use CODEOWNERS file in local directory + $ pizza generate insight . +``` + +### Options + +``` + -h, --help help for insight +``` + +### 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 generate](pizza_generate.md) - Generates documentation and insights from your codebase + diff --git a/docs/pizza_insights.md b/docs/pizza_insights.md index 7933480..27007db 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 codeowners config (default "~/.sauced.yaml") + -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 diff --git a/docs/pizza_insights_contributors.md b/docs/pizza_insights_contributors.md index 6c9d868..c228baa 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 codeowners config (default "~/.sauced.yaml") + -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") -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 9754648..f7d5036 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 codeowners config (default "~/.sauced.yaml") + -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") -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 8f07c9d..e64060b 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 codeowners config (default "~/.sauced.yaml") + -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") -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 54ead8f..d1c5965 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 codeowners config (default "~/.sauced.yaml") + -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 diff --git a/docs/pizza_version.md b/docs/pizza_version.md index ce6c725..51efa17 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 codeowners config (default "~/.sauced.yaml") + -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 diff --git a/justfile b/justfile index a5b8a10..df7a237 100644 --- a/justfile +++ b/justfile @@ -11,14 +11,15 @@ build: echo "Building for local arch" export VERSION="${RELEASE_TAG_VERSION:-dev}" - export DATETIME=$(date -u +"%Y-%m-%d %H:%M:%S") + export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S") + export SHA=$(git rev-parse HEAD) go build \ - -ldflags="-s -w" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=$(git rev-parse HEAD)'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ + -ldflags="-s -w \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza # Builds and installs the go binary for the local architecture. WARNING: requires sudo access @@ -40,17 +41,18 @@ build-darwin-amd64: echo "Building darwin amd64" export VERSION="${RELEASE_TAG_VERSION:-dev}" - export DATETIME=$(date -u +"%Y-%m-%d %H:%M:%S") + export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S") + export SHA=$(git rev-parse HEAD) export CGO_ENABLED=0 export GOOS="darwin" export GOARCH="amd64" go build \ - -ldflags="-s -w" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=$(git rev-parse HEAD)'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ + -ldflags="-s -w \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza-${GOOS}-${GOARCH} # Builds for Darwin linux (i.e., MacOS) on arm64 architecture (i.e. Apple silicon) @@ -60,17 +62,18 @@ build-darwin-arm64: echo "Building darwin arm64" export VERSION="${RELEASE_TAG_VERSION:-dev}" - export DATETIME=$(date -u +"%Y-%m-%d %H:%M:%S") + export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S") + export SHA=$(git rev-parse HEAD) export CGO_ENABLED=0 export GOOS="darwin" export GOARCH="arm64" go build \ - -ldflags="-s -w" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=$(git rev-parse HEAD)'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ + -ldflags="-s -w \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza-${GOOS}-${GOARCH} # Builds for agnostic Linux on amd64 architecture @@ -80,17 +83,18 @@ build-linux-amd64: echo "Building linux amd64" export VERSION="${RELEASE_TAG_VERSION:-dev}" - export DATETIME=$(date -u +"%Y-%m-%d %H:%M:%S") + export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S") + export SHA=$(git rev-parse HEAD) export CGO_ENABLED=0 export GOOS="linux" export GOARCH="amd64" go build \ - -ldflags="-s -w" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=$(git rev-parse HEAD)'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ + -ldflags="-s -w \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza-${GOOS}-${GOARCH} # Builds for agnostic Linux on arm64 architecture @@ -100,17 +104,18 @@ build-linux-arm64: echo "Building linux arm64" export VERSION="${RELEASE_TAG_VERSION:-dev}" - export DATETIME=$(date -u +"%Y-%m-%d %H:%M:%S") + export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S") + export SHA=$(git rev-parse HEAD) export CGO_ENABLED=0 export GOOS="linux" export GOARCH="arm64" go build \ - -ldflags="-s -w" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=$(git rev-parse HEAD)'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ + -ldflags="-s -w \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza-${GOOS}-${GOARCH} # Builds for Windows on amd64 architecture @@ -120,17 +125,18 @@ build-windows-amd64: echo "Building windows amd64" export VERSION="${RELEASE_TAG_VERSION:-dev}" - export DATETIME=$(date -u +"%Y-%m-%d %H:%M:%S") + export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S") + export SHA=$(git rev-parse HEAD) export CGO_ENABLED=0 export GOOS="windows" export GOARCH="amd64" go build \ - -ldflags="-s -w" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=$(git rev-parse HEAD)'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ + -ldflags="-s -w \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza-${GOOS}-${GOARCH} # Builds for Windows on arm64 architecture @@ -140,25 +146,34 @@ build-windows-arm64: echo "Building windows arm64" export VERSION="${RELEASE_TAG_VERSION:-dev}" - export DATETIME=$(date -u +"%Y-%m-%d %H:%M:%S") + export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S") + export SHA=$(git rev-parse HEAD) export CGO_ENABLED=0 export GOOS="windows" export GOARCH="arm64" go build \ - -ldflags="-s -w" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=$(git rev-parse HEAD)'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}'" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ + -ldflags="-s -w \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza-${GOOS}-${GOARCH} # Builds the Docker container and tags it as "dev" build-container: + #!/usr/bin/env sh + + echo "Building container" + + export VERSION="${RELEASE_TAG_VERSION:-dev}" + export DATETIME=$(date -u +"%Y-%m-%d-%H:%M:%S") + export SHA=$(git rev-parse HEAD) + docker build \ - --build-arg VERSION="$(git describe --tags --always)" \ - --build-arg SHA="$(git rev-parse HEAD)" \ - --build-arg DATETIME="$(date -u +'%Y-%m-%d %H:%M:%S')" \ + --build-arg VERSION="${VERSION}" \ + --build-arg SHA="${SHA}" \ + --build-arg DATETIME="${DATETIME}" \ --build-arg POSTHOG_PUBLIC_API_KEY="${POSTHOG_PUBLIC_API_KEY}" \ -t pizza:dev . @@ -211,8 +226,8 @@ bootstrap-telemetry: go build \ -tags telemetry \ - -ldflags="-s -w" \ - -ldflags="-X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ + -ldflags="-s -w \ + -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/telemetry-oneshot \ telemetry.go diff --git a/npm/README.md b/npm/README.md index 2d1a8e9..8f4bd4d 100644 --- a/npm/README.md +++ b/npm/README.md @@ -1,75 +1,187 @@

- Open Sauced + Open Sauced

🍕 Pizza CLI 🍕

- A Go command line interface for all things OpenSauced! + A Go command line interface for managing code ownership and project insights with OpenSauced!

-

+

GitHub code size in bytes GitHub issues - - GitHub Release - - - Discord + + GitHub Release Twitter -

+ + + +
+ +--- +# 📦 Install + +#### Homebrew + +```sh +brew install open-sauced/tap/pizza ``` -❯ pizza -A command line utility for insights, metrics, and all things OpenSauced +#### NPM -Usage: - pizza [flags] +```sh +npm i -g pizza +``` -Available Commands: - bake Use a pizza-oven to source git commits into OpenSauced - completion Generate the autocompletion script for the specified shell - help Help about any command - login Log into the CLI application via GitHub - repo-query Ask questions about a GitHub repository +You can also use `npx` to run one-off commands without installing anything: -Flags: - -h, --help help for pizza +```sh +npx pizza@latest generate codeowners . +``` -Use "pizza [command] --help" for more information about a command. +# 🍕 Pizza Action + +Use [the Pizza GitHub Action](https://github.com/open-sauced/pizza-action) for running `pizza` operations in GitHub CI/CD, +like automated `CODEOWNERS` updating and pruning: + +```yaml +jobs: + pizza-action: + runs-on: ubuntu-latest + steps: + - name: Pizza Action + uses: open-sauced/pizza-action@v2 + with: + # Optional: Whether to commit and create a PR for "CODEOWNER" changes + commit-and-pr: "true" + # Optional: Title of the PR for review by team + pr-title: "chore: update repository codeowners" ``` ---- +# 📝 Docs -### 📦 Install +- [Pizza.md](./docs/pizza.md): In depth docs on each command, option, and flag. +- [OpenSauced.pizza/docs](https://opensauced.pizza/docs/tools/pizza-cli/): Learn + how to use the Pizza command line tool and how it works with the rest of the OpenSauced + ecosystem. -There are several methods for downloading and installing the `pizza` CLI: +# ✨ Usage -#### Homebrew +## Codeowners generation + +Use the `codeowners` command to generate a GitHub style `CODEOWNERS` file or a more agnostic `OWNERS` file. +This can be used to granularly define what experts and entities have the +most context and knowledge on certain parts of a codebase. + +It's expected that there's a `.sauced.yaml` config file in the given path or in +your home directory (as `~/.sauced.yaml`): ```sh -brew install open-sauced/tap/pizza +pizza generate codeowners /path/to/local/git/repo ``` -#### NPM +Running this command will iterate the git ref-log to determine who to set as a code +owner based on the number of lines changed for that file within the given time range. +The first owner is the entity with the most lines changed. This command uses a `.sauced.yaml` configuration +to attribute emails in commits with the given entities in the config (like GitHub usernames or teams). +See [the section on the configuration schema for more details](#-configuration-schema) + +### 🚀 New in v1.4.0: Generate Config + +The `pizza generate config` command has been added to help you create `.sauced.yaml` configuration files for your projects. +This command allows you to generate configuration files with various options: ```sh -npm i -g pizza +pizza generate config /path/to/local/git/repo ``` -#### Direct script install +This command will iterate the git ref-log and inspect email signatures for commits +and, in interactive mode, ask you to attribute those users with GitHub handles. Once finished, the resulting +`.sauced.yaml` file can be used to attribute owners in a `CODEOWNERS` file during `pizza generate codeowners`. + +#### Flags: + +- `-i, --interactive`: Enter interactive mode to attribute each email manually +- `-o, --output-path string`: Set the directory for the output file +- `-h, --help`: Display help for the command + +#### Examples: + +1. Generate a config file in the current directory: + ```sh + pizza generate config ./ + ``` + +2. Generate a config file interactively: + ```sh + pizza generate config ./ -i + ``` + +3. Generate a config file from the current directory and place resulting `.sauced.yaml` in a specific output directory: + ```sh + pizza generate config ./ -o /path/to/directory + ``` + +## OpenSauced Contributor Insight from `CODEOWNERS` + +You can create an [OpenSauced Contributor Insight](https://opensauced.pizza/docs/features/contributor-insights/) +from a local `CODEOWNERS` file: -```sh -curl -fsSL https://raw.githubusercontent.com/open-sauced/pizza-cli/main/install.sh | sh ``` +pizza generate insight /path/to/repo/with/CODEOWNERS/file +``` + +This will parse the `CODEOWNERS` file and create a Contributor Insight on the OpenSauced platform. +This allows you to track insights and metrics for those codeowners, powered by OpenSauced. -This is a convenience script that can be downloaded from GitHub directly and -piped into `sh` for conveniently downloading the latest GitHub release of the -`pizza` CLI. +## Insights -Once download is completed, you can move the binary to a convenient location in -your system's `$PATH`. \ No newline at end of file +You can get metrics and insights on repositories, contributors, and more: + +``` +pizza insights [sub-command] +``` + +This powerful command lets you compose many metrics and insights together, all +powered by OpenSauced's API. Use the `--output` flag to output the results as yaml, json, csv, etc. + +# 🎷 Configuration schema + +```yaml +# Configuration for attributing commits with emails to individual entities. +# Used during "pizza generate codeowners". +attribution: + + # Keys can be GitHub usernames. + jpmcb: + + # List of emails associated with the given GitHub login. + # The commits associated with these emails will be attributed to + # this GitHub login in this yaml map. Any number of emails may be listed. + - john@opensauced.pizza + - hello@johncodes.com + + # Keys may also be GitHub teams. This is useful for orchestrating multiple + # people to a sole GitHub team. + open-sauced/engineering: + - john@opensauced.pizza + - other-user@email.com + - other-user@no-reply.github.com + + # Keys can also be agnostic names which will land as keys in "OWNERS" files + # when the "--owners-style-file" flag is set. + John McBride + - john@opensauced.pizza + +# Used during codeowners generation: if there are no code owners found +# for a file within the time range, the list of fallback entities +# will be used +attribution-fallback: + - open-sauced/engineering + - some-other-github-login +``` diff --git a/npm/package-lock.json b/npm/package-lock.json index 1549606..947def7 100644 --- a/npm/package-lock.json +++ b/npm/package-lock.json @@ -1,12 +1,12 @@ { "name": "pizza", - "version": "1.4.0", + "version": "2.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pizza", - "version": "1.4.0", + "version": "2.0.0-beta.1", "hasInstallScript": true, "license": "MIT", "bin": { diff --git a/npm/package.json b/npm/package.json index 28f6e41..6b740b1 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,7 +1,7 @@ { "name": "pizza", - "version": "1.4.0", - "description": "A command line utility for insights, metrics, and all things OpenSauced", + "version": "2.0.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", "bin": "./bin/runner.js", diff --git a/pkg/config/config.go b/pkg/config/config.go index ed722c5..98ec6f6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -9,55 +9,57 @@ import ( "gopkg.in/yaml.v3" ) -// LoadConfig loads a configuration file at a given path. It attempts to load -// 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) +// LoadConfig loads a configuration file at a given path. +// If the provided path does not exist or doesn't contain a ".sauced.yaml" file, +// "~/.sauced.yaml" from the fallback path, which is the user's home directory, is used. +// +// This function returns the config Spec, the location the spec was loaded from, and an error +func LoadConfig(path string) (*Spec, string, error) { + givenPathSpec, givenLoadedPath, givenPathErr := loadSpecAtPath(path) + if givenPathErr == nil { + return givenPathSpec, givenLoadedPath, nil + } + + homePathSpec, homeLoadedPath, homePathErr := loadSpecAtHome() + if homePathErr == nil { + return homePathSpec, homeLoadedPath, nil + } + return nil, "", fmt.Errorf("could not load config at given path: %w - could not load config at home: %w", givenPathErr, homePathErr) +} + +func loadSpecAtPath(path string) (*Spec, string, error) { config := &Spec{} absPath, err := filepath.Abs(path) if err != nil { - return nil, fmt.Errorf("error resolving absolute path: %w", err) + return nil, "", fmt.Errorf("error resolving absolute path: %s - %w", path, err) } data, err := os.ReadFile(absPath) if err != nil { - // If the file does not exist, check if the fallback path exists - if os.IsNotExist(err) { - // 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) - } - - 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 or %s", absPath, homeDirPathConfig) - } - } else { - return nil, fmt.Errorf("error reading config file: %w", err) - } + return nil, "", fmt.Errorf("error reading config file from given absolute path: %s - %w", absPath, err) } err = yaml.Unmarshal(data, config) if err != nil { - return nil, fmt.Errorf("error unmarshaling config: %w", err) + return nil, "", fmt.Errorf("error unmarshaling config at: %s - %w", absPath, err) + } + + return config, absPath, nil +} + +func loadSpecAtHome() (*Spec, string, error) { + 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") + conf, loadedPath, err := loadSpecAtPath(path) + if err != nil { + return nil, "", fmt.Errorf("could not load spec at home: %s - %w", path, err) } - return config, nil + return conf, loadedPath, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index c0da2e6..8611f5e 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,7 +47,7 @@ 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) }) @@ -78,7 +78,7 @@ attribution: _, err := os.ReadFile(fallbackPath) require.NoError(t, err) - config, err := LoadConfig(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 6383c10..9a9fb41 100644 --- a/pkg/utils/posthog.go +++ b/pkg/utils/posthog.go @@ -156,6 +156,48 @@ func (p *PosthogCliClient) CaptureFailedCodeownersGenerateContributorInsight() e return nil } +// CaptureConfigGenerate gathers telemetry on success +func (p *PosthogCliClient) CaptureConfigGenerate() error { + if p.activated { + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_generated_config", + }) + } + + return nil +} + +// CaptureConfigGenerateMode gathers what mode a user is in when generating +// either 'Automatic' (default) or 'Interactive' +func (p *PosthogCliClient) CaptureConfigGenerateMode(mode string) error { + properties := make(map[string]interface{}) + + properties["mode"] = mode + + if p.activated { + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_generated_config_mode", + Properties: properties, + }) + } + + return nil +} + +// CaptureFailedConfigGenerate gathers telemetry on failed +func (p *PosthogCliClient) CaptureFailedConfigGenerate() error { + if p.activated { + return p.client.Enqueue(posthog.Capture{ + DistinctId: p.uniqueID, + Event: "pizza_cli_failed_to_generate_config", + }) + } + + return nil +} + // CaptureInsights gathers telemetry on successful Insights command runs func (p *PosthogCliClient) CaptureInsights() error { if p.activated { diff --git a/telemetry.go b/telemetry.go index 71d910a..7f2c74a 100644 --- a/telemetry.go +++ b/telemetry.go @@ -36,6 +36,21 @@ func main() { panic(err) } + err = client.CaptureConfigGenerate() + if err != nil { + panic(err) + } + + err = client.CaptureFailedConfigGenerate() + if err != nil { + panic(err) + } + + err = client.CaptureConfigGenerateMode("interactive") + if err != nil { + panic(err) + } + err = client.CaptureCodeownersGenerateAuth("test-user") if err != nil { panic(err)