package epics import ( "context" "fmt" "strconv" "strings" "github.com/google/go-github/v47/github" "edge-infra.dev/pkg/f8n/devinfra/jack/constants" guestservices "edge-infra.dev/pkg/f8n/devinfra/jack/guest_services" "edge-infra.dev/pkg/f8n/devinfra/jack/plugin" "edge-infra.dev/pkg/lib/logging" ) func labelAdded(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error { repo := event.GetRepo() repoName := repo.GetName() repoID := repo.GetID() repoOwner := repo.GetOwner().GetLogin() issueNumber := event.GetIssue().GetNumber() issueLabelName := event.GetLabel().GetName() // check if the new labels is a parent label if !guestservices.IsParentLabel(issueLabelName) { return nil } log.Info("Running epics plugin") if event.GetSender().GetType() == constants.Bot { log.Info("a bot added the label ignoring") return nil } hasParentLabel, labels := guestservices.CheckForParentLabel(issueLabelName, event.Issue.Labels) log.Info(fmt.Sprintf("%+v", labels)) // remove the old parent label if hasParentLabel { return swapParentLabels(ctx, client, event, labels) } log.Info(fmt.Sprintf("Epic %v initializing", issueNumber)) // Create a new epic and toggle the issue open status body := event.GetIssue().GetBody() parentLabels := event.GetIssue().Labels // isEpic := guestservices.HasEpicLabel(event.Issue.Labels) parent := guestservices.ParentChild{} parent.New(body, parentLabels, constants.Preamble, constants.Postamble, repoID) // if it's an epic omit the parent section parentBody := parent.ToString() log.Info(fmt.Sprintf("Epic %v initialized with the jackbot epic template", issueNumber)) githubIssueBody := &github.IssueRequest{Body: github.String(parentBody)} _, _, err := client.Issues().Edit(ctx, repoOwner, repoName, issueNumber, githubIssueBody) if err != nil { return err } return nil } func swapParentLabels(ctx context.Context, client plugin.GithubClientInterface, event github.IssuesEvent, labels []string) error { body := event.GetIssue().GetBody() repo := event.GetRepo() repoName := repo.GetName() repoID := repo.GetID() repoOwner := repo.GetOwner().GetLogin() issueNumber := event.GetIssue().GetNumber() // put the labels in the label struct before creating the new parent var fl []*github.Label for _, label := range labels { fl = append(fl, &github.Label{Name: github.String(label)}) } parent := guestservices.ParentChild{} parent.New(body, fl, constants.Preamble, constants.Postamble, repoID) parentBody := parent.ToString() githubIssueBody := &github.IssueRequest{Body: github.String(parentBody), Labels: &labels} _, _, err := client.Issues().Edit(ctx, repoOwner, repoName, issueNumber, githubIssueBody) if err != nil { return err } return nil } func labelRemoved(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error { issueLabelName := event.GetLabel().GetName() // Get the old and new issue bodies issueNumber := event.GetIssue().GetNumber() repo := event.GetRepo() repoName := repo.GetName() repoOwner := repo.GetOwner().GetLogin() // if the label being removed isnt a parent label bounce if !guestservices.IsParentLabel(issueLabelName) { return nil } // if there's an active parent label bounce // (this allows for swapping parent labels without losing the list) isParent, _ := guestservices.CheckForParentLabel(issueLabelName, event.Issue.Labels) if isParent { return nil } // otherwise remove the parent block err := handleEpicLabelRemoval(ctx, log, client, event) if err != nil { log.Error(err, "Failed to update issue desc") return err } labelBody := createNewLabelBody(event.GetIssue().Labels) githubIssueBody := &github.IssueRequest{Labels: &labelBody} _, _, err = client.Issues().Edit(ctx, repoOwner, repoName, issueNumber, githubIssueBody) if err != nil { log.Error(err, "Failed to update issue desc") return err } return nil } func handleEpicLabelRemoval(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error { log.Info("Removing the jackbot epic template from issue") // Get the old and new issue bodys issueBody := event.GetIssue().GetBody() issueLabels := event.GetIssue().Labels issueNumber := event.GetIssue().GetNumber() issueTitle := event.GetIssue().GetTitle() repo := event.GetRepo() repoID := repo.GetID() repoName := repo.GetName() repoOwner := repo.GetOwner().GetLogin() issueCommenter := event.GetSender().GetLogin() sender := guestservices.ParentChild{} sender.New(issueBody, issueLabels, constants.Preamble, constants.Postamble, repoID) newEpicBody, clearedChildren := sender.ClearChildren() log.Info(fmt.Sprintf("Parent %v had its children removed", issueNumber)) clearedIssueComments := []string{} for _, v := range clearedChildren { foundIssue, _, err := client.Issues().Get(ctx, repoOwner, repoName, v.Number) if err != nil { log.Error(err, "Failed to get the selected issue") return err } receiver := guestservices.ParentChild{} receiver.New(foundIssue.GetBody(), foundIssue.Labels, constants.Preamble, constants.Postamble, repoID) receiver.Remove(issueNumber, repoID) newIssueBody := receiver.ToString() log.Info(fmt.Sprintf("Removed Epic %v from issue %v", issueNumber, v.Number)) err = updateIssue(ctx, repoOwner, repoName, v.Number, newIssueBody, client) if err != nil { log.Error(err, "Failed to update issue with new body") return err } msg := fmt.Sprintf("@%s has removed the parent label from ```#%d - %s``` and it has been removed from this issue.", issueCommenter, issueNumber, issueTitle) prComment := github.IssueComment{ Body: &msg, } if _, _, err := client.Issues().CreateComment(ctx, repoOwner, repoName, v.Number, &prComment); err != nil { log.Error(err, "Failed to comment on issue") return err } clearedIssueComments = append(clearedIssueComments, "#"+strconv.Itoa(v.Number)) } clearedCommentSection := strings.Join(clearedIssueComments, ", ") // Update the issue with the new body err := updateIssue(ctx, repoOwner, repoName, issueNumber, newEpicBody, client) if err != nil { log.Error(err, "Failed to update child with new body") return err } msg := fmt.Sprintf("@%s has removed the parent label and the following issues have been removed: %s", issueCommenter, clearedCommentSection) prComment := github.IssueComment{ Body: &msg, } if _, _, err := client.Issues().CreateComment(ctx, repoOwner, repoName, issueNumber, &prComment); err != nil { log.Error(err, "Failed to comment on issue") return err } return nil } func updateIssue(ctx context.Context, repoOwner string, repoName string, issueNumber int, newBody string, client plugin.GithubClientInterface) error { githubIssueBody := &github.IssueRequest{Body: github.String(newBody)} _, _, err := client.Issues().Edit(ctx, repoOwner, repoName, issueNumber, githubIssueBody) return err } func verifyEditedEpic(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error { log.Info("Checking edits made to an issue") // If the sender is a bot ignore it senderType := event.GetSender().GetType() if senderType == constants.Bot { return nil } err := ParseJackBlock(ctx, log, client, event) if err != nil { log.Error(err, "Failed to parse jack's block.") return err } return nil } func createNewLabelBody(labels []*github.Label) []string { label := []string{} // Add all existing labels to new label slice and toggle sizeLabel for _, v := range labels { name := v.GetName() label = append(label, name) } return label } func ParseJackBlock(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error { changes := event.GetChanges() changedBody := changes.GetBody().GetFrom() // Check if any changes were made at all if changedBody == "" { log.Info("No changed body. Ignoring.") } // Get the old and new issue bodies oldBody := strings.ReplaceAll(changes.GetBody().GetFrom(), "\r\n", "\n") newBody := strings.ReplaceAll(event.GetIssue().GetBody(), "\r\n", "\n") issueNumber := event.GetIssue().GetNumber() repo := event.GetRepo() repoName := repo.GetName() repoOwner := repo.GetOwner().GetLogin() // Check if the issue body is empty if oldBody == "" && newBody == "" { log.Info("Issue Body is empty.") cleanJackBlock := constants.Preamble + constants.Postamble err := updateIssue(ctx, repoOwner, repoName, issueNumber, cleanJackBlock, client) if err != nil { log.Error(err, "Failed to update issue") return err } emptyIssueCommentBody := "An error has occurred. The issue body cannot be left empty. The previous issue body cannot be recovered. Please add a parent or child to continue using jack." emptyIssueComment := github.IssueComment{ Body: &emptyIssueCommentBody, } if _, _, err := client.Issues().CreateComment(ctx, repoOwner, *repo.Name, issueNumber, &emptyIssueComment); err != nil { log.Error(err, "Failed to comment on issue") return err } return nil } var newBodyPostamble []string var userPostamble string newBodySplit := strings.SplitN(newBody, "", 2) //Check the length in case the JACKBOT tag has been deleted index := 1 if len(newBodySplit) == 1 { index = 0 } //User userPostamble is a newline if the ENDJACKBOT tag gets deleted userPostamble = "\n" slice := newBodySplit[index] if strings.Contains(slice, "") { newBodyPostamble = strings.SplitN(slice, "", 2) userPostamble = newBodyPostamble[1] } //userPreamble is the section above the jack block. The user is allowed to edit this section userPreamble := newBodySplit[0] //correctPreamble includes JACKBOT and everything after it var correctPreamble string var correctPreambleSplit []string var correctBody string //correctBodySplit splits at the JACKBOT tag and drops everything before it correctBodySplit := strings.SplitN(oldBody, "", 2) if len(correctBodySplit) != 2 { return nil } correctPreamble = "" + correctBodySplit[1] //Here we drop everything after the ENDJACKBOT tag so we can append the userPreamble later correctPreambleSplit = strings.SplitN(correctPreamble, "", 2) //correctBody is the default Jack Body. The user is not allowed to edit this section correctBody = correctPreambleSplit[0] + "" //Checking if newBody has the default Jack body. If not, jack block changes are reverted. User is then notified if !strings.Contains(newBody, correctBody) { //The permitted userPreamble and the default jack block are combined to create the new body. newBody = userPreamble + correctBody + userPostamble //This is the duplication check. If the index is 1, the userPreamble would be the entire issue body. if index == 0 { newBody = correctBody } err := updateIssue(ctx, repoOwner, repoName, issueNumber, newBody, client) if err != nil { log.Error(err, "Failed to update issue") return err } revertCommentBody := "Unauthorized manual edit to Jack's block attempted. Changes inside the block have been removed. Please place all edits above JACKBOT." revertIssueComment := github.IssueComment{ Body: &revertCommentBody, } if _, _, err := client.Issues().CreateComment(ctx, repoOwner, *repo.Name, issueNumber, &revertIssueComment); err != nil { log.Error(err, "Failed to comment on issue") return err } } return nil }