Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement pizza offboard command #207

Merged
merged 15 commits into from
Oct 7, 2024
139 changes: 139 additions & 0 deletions cmd/offboard/offboard.go
Original file line number Diff line number Diff line change
@@ -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{
zeucapua marked this conversation as resolved.
Show resolved Hide resolved
Use: "offboard <username/email> [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) {
zeucapua marked this conversation as resolved.
Show resolved Hide resolved
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 {
zeucapua marked this conversation as resolved.
Show resolved Hide resolved
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")
zeucapua marked this conversation as resolved.
Show resolved Hide resolved
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
}
87 changes: 87 additions & 0 deletions cmd/offboard/output.go
Original file line number Diff line number Diff line change
@@ -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 {
zeucapua marked this conversation as resolved.
Show resolved Hide resolved
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 {
zeucapua marked this conversation as resolved.
Show resolved Hide resolved
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
}
2 changes: 2 additions & 0 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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()
Expand Down
22 changes: 22 additions & 0 deletions pkg/utils/posthog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading