...

Source file src/edge-infra.dev/pkg/f8n/devinfra/jack/plugin/epics/issues.go

Documentation: edge-infra.dev/pkg/f8n/devinfra/jack/plugin/epics

     1  package epics
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/google/go-github/v47/github"
    10  
    11  	"edge-infra.dev/pkg/f8n/devinfra/jack/constants"
    12  	guestservices "edge-infra.dev/pkg/f8n/devinfra/jack/guest_services"
    13  	"edge-infra.dev/pkg/f8n/devinfra/jack/plugin"
    14  	"edge-infra.dev/pkg/lib/logging"
    15  )
    16  
    17  func labelAdded(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error {
    18  	repo := event.GetRepo()
    19  	repoName := repo.GetName()
    20  	repoID := repo.GetID()
    21  	repoOwner := repo.GetOwner().GetLogin()
    22  	issueNumber := event.GetIssue().GetNumber()
    23  	issueLabelName := event.GetLabel().GetName()
    24  
    25  	// check if the new labels is a parent label
    26  	if !guestservices.IsParentLabel(issueLabelName) {
    27  		return nil
    28  	}
    29  	log.Info("Running epics plugin")
    30  
    31  	if event.GetSender().GetType() == constants.Bot {
    32  		log.Info("a bot added the label ignoring")
    33  		return nil
    34  	}
    35  
    36  	hasParentLabel, labels := guestservices.CheckForParentLabel(issueLabelName, event.Issue.Labels)
    37  	log.Info(fmt.Sprintf("%+v", labels))
    38  
    39  	// remove the old parent label
    40  	if hasParentLabel {
    41  		return swapParentLabels(ctx, client, event, labels)
    42  	}
    43  	log.Info(fmt.Sprintf("Epic %v initializing", issueNumber))
    44  	// Create a new epic and toggle the issue open status
    45  	body := event.GetIssue().GetBody()
    46  	parentLabels := event.GetIssue().Labels
    47  	// isEpic := guestservices.HasEpicLabel(event.Issue.Labels)
    48  
    49  	parent := guestservices.ParentChild{}
    50  	parent.New(body, parentLabels, constants.Preamble, constants.Postamble, repoID)
    51  
    52  	// if it's an epic omit the parent section
    53  	parentBody := parent.ToString()
    54  
    55  	log.Info(fmt.Sprintf("Epic %v initialized with the jackbot epic template", issueNumber))
    56  
    57  	githubIssueBody := &github.IssueRequest{Body: github.String(parentBody)}
    58  	_, _, err := client.Issues().Edit(ctx, repoOwner, repoName, issueNumber, githubIssueBody)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	return nil
    64  }
    65  
    66  func swapParentLabels(ctx context.Context, client plugin.GithubClientInterface, event github.IssuesEvent, labels []string) error {
    67  	body := event.GetIssue().GetBody()
    68  	repo := event.GetRepo()
    69  	repoName := repo.GetName()
    70  	repoID := repo.GetID()
    71  	repoOwner := repo.GetOwner().GetLogin()
    72  	issueNumber := event.GetIssue().GetNumber()
    73  
    74  	// put the labels in the label struct before creating the new parent
    75  	var fl []*github.Label
    76  	for _, label := range labels {
    77  		fl = append(fl, &github.Label{Name: github.String(label)})
    78  	}
    79  
    80  	parent := guestservices.ParentChild{}
    81  	parent.New(body, fl, constants.Preamble, constants.Postamble, repoID)
    82  	parentBody := parent.ToString()
    83  
    84  	githubIssueBody := &github.IssueRequest{Body: github.String(parentBody), Labels: &labels}
    85  	_, _, err := client.Issues().Edit(ctx, repoOwner, repoName, issueNumber, githubIssueBody)
    86  	if err != nil {
    87  		return err
    88  	}
    89  	return nil
    90  }
    91  
    92  func labelRemoved(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error {
    93  	issueLabelName := event.GetLabel().GetName()
    94  
    95  	// Get the old and new issue bodies
    96  	issueNumber := event.GetIssue().GetNumber()
    97  	repo := event.GetRepo()
    98  	repoName := repo.GetName()
    99  	repoOwner := repo.GetOwner().GetLogin()
   100  
   101  	// if the label being removed isnt a parent label bounce
   102  	if !guestservices.IsParentLabel(issueLabelName) {
   103  		return nil
   104  	}
   105  
   106  	// if there's an active parent label bounce
   107  	// (this allows for swapping parent labels without losing the list)
   108  	isParent, _ := guestservices.CheckForParentLabel(issueLabelName, event.Issue.Labels)
   109  	if isParent {
   110  		return nil
   111  	}
   112  
   113  	// otherwise remove the parent block
   114  	err := handleEpicLabelRemoval(ctx, log, client, event)
   115  	if err != nil {
   116  		log.Error(err, "Failed to update issue desc")
   117  		return err
   118  	}
   119  	labelBody := createNewLabelBody(event.GetIssue().Labels)
   120  
   121  	githubIssueBody := &github.IssueRequest{Labels: &labelBody}
   122  	_, _, err = client.Issues().Edit(ctx, repoOwner, repoName, issueNumber, githubIssueBody)
   123  	if err != nil {
   124  		log.Error(err, "Failed to update issue desc")
   125  		return err
   126  	}
   127  	return nil
   128  }
   129  
   130  func handleEpicLabelRemoval(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error {
   131  	log.Info("Removing the jackbot epic template from issue")
   132  
   133  	// Get the old and new issue bodys
   134  	issueBody := event.GetIssue().GetBody()
   135  	issueLabels := event.GetIssue().Labels
   136  	issueNumber := event.GetIssue().GetNumber()
   137  	issueTitle := event.GetIssue().GetTitle()
   138  	repo := event.GetRepo()
   139  	repoID := repo.GetID()
   140  	repoName := repo.GetName()
   141  	repoOwner := repo.GetOwner().GetLogin()
   142  	issueCommenter := event.GetSender().GetLogin()
   143  
   144  	sender := guestservices.ParentChild{}
   145  	sender.New(issueBody, issueLabels, constants.Preamble, constants.Postamble, repoID)
   146  	newEpicBody, clearedChildren := sender.ClearChildren()
   147  
   148  	log.Info(fmt.Sprintf("Parent %v had its children removed", issueNumber))
   149  
   150  	clearedIssueComments := []string{}
   151  	for _, v := range clearedChildren {
   152  		foundIssue, _, err := client.Issues().Get(ctx, repoOwner, repoName, v.Number)
   153  		if err != nil {
   154  			log.Error(err, "Failed to get the selected issue")
   155  			return err
   156  		}
   157  
   158  		receiver := guestservices.ParentChild{}
   159  		receiver.New(foundIssue.GetBody(), foundIssue.Labels, constants.Preamble, constants.Postamble, repoID)
   160  
   161  		receiver.Remove(issueNumber, repoID)
   162  		newIssueBody := receiver.ToString()
   163  		log.Info(fmt.Sprintf("Removed Epic %v from issue %v", issueNumber, v.Number))
   164  
   165  		err = updateIssue(ctx, repoOwner, repoName, v.Number, newIssueBody, client)
   166  		if err != nil {
   167  			log.Error(err, "Failed to update issue with new body")
   168  			return err
   169  		}
   170  
   171  		msg := fmt.Sprintf("@%s has removed the parent label from ```#%d - %s``` and it has been removed from this issue.", issueCommenter, issueNumber, issueTitle)
   172  		prComment := github.IssueComment{
   173  			Body: &msg,
   174  		}
   175  		if _, _, err := client.Issues().CreateComment(ctx, repoOwner, repoName, v.Number, &prComment); err != nil {
   176  			log.Error(err, "Failed to comment on issue")
   177  			return err
   178  		}
   179  
   180  		clearedIssueComments = append(clearedIssueComments, "#"+strconv.Itoa(v.Number))
   181  	}
   182  	clearedCommentSection := strings.Join(clearedIssueComments, ", ")
   183  
   184  	// Update the issue with the new body
   185  	err := updateIssue(ctx, repoOwner, repoName, issueNumber, newEpicBody, client)
   186  	if err != nil {
   187  		log.Error(err, "Failed to update child with new body")
   188  		return err
   189  	}
   190  
   191  	msg := fmt.Sprintf("@%s has removed the parent label and the following issues have been removed: %s", issueCommenter, clearedCommentSection)
   192  	prComment := github.IssueComment{
   193  		Body: &msg,
   194  	}
   195  	if _, _, err := client.Issues().CreateComment(ctx, repoOwner, repoName, issueNumber, &prComment); err != nil {
   196  		log.Error(err, "Failed to comment on issue")
   197  		return err
   198  	}
   199  
   200  	return nil
   201  }
   202  
   203  func updateIssue(ctx context.Context, repoOwner string, repoName string, issueNumber int, newBody string, client plugin.GithubClientInterface) error {
   204  	githubIssueBody := &github.IssueRequest{Body: github.String(newBody)}
   205  	_, _, err := client.Issues().Edit(ctx, repoOwner, repoName, issueNumber, githubIssueBody)
   206  	return err
   207  }
   208  
   209  func verifyEditedEpic(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error {
   210  	log.Info("Checking edits made to an issue")
   211  	// If the sender is a bot ignore it
   212  	senderType := event.GetSender().GetType()
   213  	if senderType == constants.Bot {
   214  		return nil
   215  	}
   216  	err := ParseJackBlock(ctx, log, client, event)
   217  	if err != nil {
   218  		log.Error(err, "Failed to parse jack's block.")
   219  		return err
   220  	}
   221  	return nil
   222  }
   223  
   224  func createNewLabelBody(labels []*github.Label) []string {
   225  	label := []string{}
   226  
   227  	// Add all existing labels to new label slice and toggle sizeLabel
   228  	for _, v := range labels {
   229  		name := v.GetName()
   230  		label = append(label, name)
   231  	}
   232  
   233  	return label
   234  }
   235  
   236  func ParseJackBlock(ctx context.Context, log logging.EdgeLogger, client plugin.GithubClientInterface, event github.IssuesEvent) error {
   237  	changes := event.GetChanges()
   238  	changedBody := changes.GetBody().GetFrom()
   239  	// Check if any changes were made at all
   240  	if changedBody == "" {
   241  		log.Info("No changed body. Ignoring.")
   242  	}
   243  
   244  	// Get the old and new issue bodies
   245  	oldBody := strings.ReplaceAll(changes.GetBody().GetFrom(), "\r\n", "\n")
   246  	newBody := strings.ReplaceAll(event.GetIssue().GetBody(), "\r\n", "\n")
   247  
   248  	issueNumber := event.GetIssue().GetNumber()
   249  	repo := event.GetRepo()
   250  	repoName := repo.GetName()
   251  	repoOwner := repo.GetOwner().GetLogin()
   252  
   253  	// Check if the issue body is empty
   254  	if oldBody == "" && newBody == "" {
   255  		log.Info("Issue Body is empty.")
   256  		cleanJackBlock := constants.Preamble + constants.Postamble
   257  		err := updateIssue(ctx, repoOwner, repoName, issueNumber, cleanJackBlock, client)
   258  		if err != nil {
   259  			log.Error(err, "Failed to update issue")
   260  			return err
   261  		}
   262  		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."
   263  		emptyIssueComment := github.IssueComment{
   264  			Body: &emptyIssueCommentBody,
   265  		}
   266  		if _, _, err := client.Issues().CreateComment(ctx, repoOwner, *repo.Name, issueNumber, &emptyIssueComment); err != nil {
   267  			log.Error(err, "Failed to comment on issue")
   268  			return err
   269  		}
   270  		return nil
   271  	}
   272  	var newBodyPostamble []string
   273  	var userPostamble string
   274  	newBodySplit := strings.SplitN(newBody, "<!-- JACKBOT -->", 2)
   275  
   276  	//Check the length in case the JACKBOT tag has been deleted
   277  
   278  	index := 1
   279  	if len(newBodySplit) == 1 {
   280  		index = 0
   281  	}
   282  
   283  	//User userPostamble is a newline if the ENDJACKBOT tag gets deleted
   284  	userPostamble = "\n"
   285  	slice := newBodySplit[index]
   286  
   287  	if strings.Contains(slice, "<!-- ENDJACKBOT -->") {
   288  		newBodyPostamble = strings.SplitN(slice, "<!-- ENDJACKBOT -->", 2)
   289  		userPostamble = newBodyPostamble[1]
   290  	}
   291  
   292  	//userPreamble is the section above the jack block. The user is allowed to edit this section
   293  	userPreamble := newBodySplit[0]
   294  	//correctPreamble includes JACKBOT and everything after it
   295  	var correctPreamble string
   296  	var correctPreambleSplit []string
   297  	var correctBody string
   298  
   299  	//correctBodySplit splits at the JACKBOT tag and drops everything before it
   300  	correctBodySplit := strings.SplitN(oldBody, "<!-- JACKBOT -->", 2)
   301  	if len(correctBodySplit) != 2 {
   302  		return nil
   303  	}
   304  	correctPreamble = "<!-- JACKBOT -->" + correctBodySplit[1]
   305  	//Here we drop everything after the ENDJACKBOT tag so we can append the userPreamble later
   306  	correctPreambleSplit = strings.SplitN(correctPreamble, "<!-- ENDJACKBOT -->", 2)
   307  	//correctBody is the default Jack Body. The user is not allowed to edit this section
   308  	correctBody = correctPreambleSplit[0] + "<!-- ENDJACKBOT -->"
   309  
   310  	//Checking if newBody has the default Jack body. If not, jack block changes are reverted. User is then notified
   311  	if !strings.Contains(newBody, correctBody) {
   312  		//The permitted userPreamble and the default jack block are combined to create the new body.
   313  		newBody = userPreamble + correctBody + userPostamble
   314  
   315  		//This is the duplication check. If the index is 1, the userPreamble would be the entire issue body.
   316  		if index == 0 {
   317  			newBody = correctBody
   318  		}
   319  		err := updateIssue(ctx, repoOwner, repoName, issueNumber, newBody, client)
   320  		if err != nil {
   321  			log.Error(err, "Failed to update issue")
   322  			return err
   323  		}
   324  		revertCommentBody := "Unauthorized manual edit to Jack's block attempted. Changes inside the block have been removed. Please place all edits above JACKBOT."
   325  		revertIssueComment := github.IssueComment{
   326  			Body: &revertCommentBody,
   327  		}
   328  		if _, _, err := client.Issues().CreateComment(ctx, repoOwner, *repo.Name, issueNumber, &revertIssueComment); err != nil {
   329  			log.Error(err, "Failed to comment on issue")
   330  			return err
   331  		}
   332  	}
   333  	return nil
   334  }
   335  

View as plain text