diff --git a/plugins/deployed-labeler/README.md b/plugins/deployed-labeler/README.md index def6195..3bb578e 100644 --- a/plugins/deployed-labeler/README.md +++ b/plugins/deployed-labeler/README.md @@ -7,7 +7,7 @@ Accepts `POST` requests, while requiring to parameters: - `commit`: Commit that has just been deployed to production. - `team`: Which team just deployed to production. -`deployed-labeler` will look for the last 100 commits of the repository's default branch, alongside their associated Pull Requests and labels. +`deployed-labeler` will look for the last 500 commits of the repository's default branch, alongside their associated Pull Requests and labels. ![image](https://user-images.githubusercontent.com/24193764/139254510-9f8ed8e1-e9ac-4177-b447-49932b804edd.png) @@ -51,9 +51,15 @@ docker run \ And `curl` the endpoint -```sh -curl -XPOST "http://localhost:8080/deployed?commit=01f4897c5323433e7831ca948f7d340c3c762885&team=webapp" -``` +- Check for unlabeled pull requests: + ```sh + curl "http://localhost:8080/deployed?commit=01f4897c5323433e7831ca948f7d340c3c762885&team=webapp" + ``` + +- Upload pull request labels: + ```sh + curl -XPOST "http://localhost:8080/deployed?commit=01f4897c5323433e7831ca948f7d340c3c762885&team=webapp" + ``` ## Deploying diff --git a/plugins/deployed-labeler/main.go b/plugins/deployed-labeler/main.go index 3fadd1d..08607c6 100644 --- a/plugins/deployed-labeler/main.go +++ b/plugins/deployed-labeler/main.go @@ -28,6 +28,7 @@ type options struct { dryRun bool github prowflagutil.GitHubOptions hmacSecret string + logLevel string } func newOptions() *options { @@ -36,6 +37,7 @@ func newOptions() *options { fs.IntVar(&o.port, "port", 8080, "Port to listen to.") fs.BoolVar(&o.dryRun, "dry-run", false, "Dry run for testing (uses API tokens but does not mutate).") fs.StringVar(&o.hmacSecret, "hmac", "/etc/webhook/hmac", "Path to the file containing the GitHub HMAC secret.") + fs.StringVar(&o.logLevel, "log-level", "debug", "Application log level") for _, group := range []flagutil.OptionGroup{&o.github} { group.AddFlags(fs) @@ -56,7 +58,7 @@ func main() { o := newOptions() logrus.SetFormatter(&logrus.JSONFormatter{}) - logrus.SetLevel(logrus.DebugLevel) + logrus.ParseLevel(o.logLevel) log := logrus.StandardLogger().WithField("plugin", pluginName) secretAgent := &secret.Agent{} @@ -90,35 +92,75 @@ func main() { } func (s *server) markDeployedPR(w http.ResponseWriter, req *http.Request) { + var commitSHA string + var team string + for k, v := range req.URL.Query() { + switch k { + case "commit": + commitSHA = v[0] + case "team": + team = v[0] + default: + s.log.Warnf("Unrecognized parameter received: %s", k) + } + } + + if team == "" || commitSHA == "" { + s.log.WithFields( + logrus.Fields{ + "team": team, + "commit": commitSHA, + }, + ).Error("team and commit are required parameters") + + w.WriteHeader(http.StatusPreconditionFailed) + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + enc.Encode(map[string]string{ + "error": "team and commit are required parameters", + }) + return + } + var err error switch req.Method { - case "POST": - var commitSHA string - var team string - for k, v := range req.URL.Query() { - switch k { - case "commit": - commitSHA = v[0] - case "team": - team = v[0] - default: - s.log.Warnf("Unrecognized parameter received: %s", k) + case "GET": + var msg struct { + PRs struct { + Labeled []string `json:"labeled"` + Unlabeled []string `json:"unlabeled"` } + Error string `json:"error"` } - if team == "" || commitSHA == "" { - w.WriteHeader(http.StatusPreconditionFailed) - w.Write([]byte(http.StatusText(http.StatusPreconditionFailed))) - s.log.Errorf("team and commit are required parameters. Team: %v, commit: %v", team, commitSHA) - break + + msg.PRs.Labeled, msg.PRs.Unlabeled, err = s.handleGetUnmarkedPRs(req.Context(), commitSHA, team) + if err != nil { + msg.Error = err.Error() + w.WriteHeader(http.StatusUnprocessableEntity) } + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + enc.Encode(msg) + + case "POST": + var errs []error var msg struct { DeployedPRs struct { Team []string `json:"team"` All []string `json:"all"` } `json:"deployedPRs"` - Errors []error `json:"errors"` + Errors []string `json:"errors"` + } + msg.DeployedPRs.Team, msg.DeployedPRs.All, errs = s.handleMarkDeployedPRs(req.Context(), commitSHA, team) + + msg.Errors = make([]string, 0, len(errs)) + for _, err := range errs { + msg.Errors = append(msg.Errors, err.Error()) + } + + if len(msg.Errors) > 0 { + w.WriteHeader(http.StatusUnprocessableEntity) } - msg.DeployedPRs.Team, msg.DeployedPRs.All, msg.Errors = s.handleMarkDeployedPRs(req.Context(), commitSHA, team) enc := json.NewEncoder(w) enc.SetIndent("", " ") @@ -140,6 +182,17 @@ func (s *server) handleMarkDeployedPRs(ctx context.Context, commitSHA, team stri return s.updatePullRequests(prs, team) } +func (s *server) handleGetUnmarkedPRs(ctx context.Context, commitSHA, team string) (labeledPRs, unlabeledPRs []string, err error) { + prs, err := s.getMergedPRs(ctx, commitSHA) + if err != nil { + return nil, nil, err + } + + labeledPRs, unlabeledPRs = s.findTeamPullRequests(prs, team) + + return labeledPRs, unlabeledPRs, nil +} + const ( org = "gitpod-io" repo = "gitpod" @@ -193,6 +246,26 @@ func (s *server) updatePullRequests(prs []pullRequest, team string) (teamDeploye return } +// Find labeled and unlabeled pull requests for the given team. +func (s *server) findTeamPullRequests(prs []pullRequest, team string) (labeled, unlabeled []string) { + for _, pr := range prs { + + lblTeam := teamLabel(team) + if _, belongs := pr.Labels[lblTeam]; !belongs { + s.log.Infof("PR %v does not belong to %v, skipping it", pr.Number, team) + continue + } + + teamDeployedLabel := deployedLabel(team) + if _, hasLabel := pr.Labels[teamDeployedLabel]; !hasLabel { + unlabeled = append(unlabeled, pr.URL) + } else { + labeled = append(labeled, pr.URL) + } + } + return +} + func deployedLabel(team string) string { return fmt.Sprintf("%s: %s", labelDeployed, team) } func teamLabel(team string) string { return labelPrefixTeam + team } @@ -212,8 +285,12 @@ func (s *server) getMergedPRs(ctx context.Context, commitSHA string) ([]pullRequ var commits []commitNodes // we get 100 commits per page - // 3x100 = 300 in total - for i := 0; i < 3; i++ { + // 5x100 = 500 in total + // + // Note that this value is sensitive to the commit rate of the given repo and the interval at which teams deploy; + // if more than 500 commits are merged within a week and a given team only deploys on a weekly basis then some commits + // might not be labeled. + for i := 0; i < 5; i++ { err := s.gh.Query(ctx, &q, variables) if err != nil { s.log.WithError(err).Error("Error running query.") @@ -239,6 +316,14 @@ func (s *server) getMergedPRs(ctx context.Context, commitSHA string) ([]pullRequ pr.Labels[string(lbl.Name)] = struct{}{} } res = append(res, pr) + + s.log.WithFields( + logrus.Fields{ + "Number": pr.Number, + "URL": pr.URL, + "Labels": pr.Labels, + }, + ).Info("Added pull request for commit %s", c) } }