golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/task/tagtelemetry.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package task
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"strings"
    12  	"time"
    13  
    14  	"golang.org/x/build/gerrit"
    15  	wf "golang.org/x/build/internal/workflow"
    16  	"golang.org/x/mod/semver"
    17  )
    18  
    19  // TagTelemetryTasks implements a new workflow definition to tag
    20  // x/telemetry/config whenever the generated config.json changes.
    21  type TagTelemetryTasks struct {
    22  	Gerrit     GerritClient
    23  	CloudBuild CloudBuildClient
    24  }
    25  
    26  func (t *TagTelemetryTasks) NewDefinition() *wf.Definition {
    27  	wd := wf.New()
    28  
    29  	reviewers := wf.Param(wd, reviewersParam)
    30  	changeID := wf.Task1(wd, "generate config CL", t.GenerateConfig, reviewers)
    31  	submitted := wf.Action1(wd, "await config CL submission", t.AwaitSubmission, changeID)
    32  	tag := wf.Task0(wd, "tag if appropriate", t.MaybeTag, wf.After(submitted))
    33  	wf.Output(wd, "tag", tag)
    34  
    35  	return wd
    36  }
    37  
    38  // GenerateConfig runs the upload config generator in a buildlet, extracts the
    39  // resulting config.json, and creates a CL with the result if anything changed.
    40  //
    41  // It returns the change ID, or "" if the CL was not created.
    42  func (t *TagTelemetryTasks) GenerateConfig(ctx *wf.TaskContext, reviewers []string) (string, error) {
    43  	const clTitle = "config: regenerate upload config"
    44  
    45  	// Query for an existing pending config CL, to avoid duplication.
    46  	//
    47  	// Only wait a week, because configs are volatile: we really want to update
    48  	// them within a week.
    49  	query := fmt.Sprintf(`message:%q status:open owner:gobot@golang.org repo:telemetry -age:7d`, clTitle)
    50  	changes, err := t.Gerrit.QueryChanges(ctx, query)
    51  	if err != nil {
    52  		return "", err
    53  	}
    54  	if len(changes) > 0 {
    55  		ctx.Printf("not creating CL: found existing CL %d", changes[0].ChangeNumber)
    56  		return "", nil
    57  	}
    58  
    59  	const script = `
    60  cp config/config.json config/config.json.before
    61  go run ./internal/configgen -w
    62  `
    63  
    64  	build, err := t.CloudBuild.RunScript(ctx, script, "telemetry", []string{"config/config.json.before", "config/config.json"})
    65  	if err != nil {
    66  		return "", err
    67  	}
    68  
    69  	outputs, err := buildToOutputs(ctx, t.CloudBuild, build)
    70  	if err != nil {
    71  		return "", err
    72  	}
    73  
    74  	before, after := outputs["config/config.json.before"], outputs["config/config.json"]
    75  	if before == after {
    76  		ctx.Printf("not creating CL: config has not changed")
    77  		return "", nil
    78  	}
    79  
    80  	changeInput := gerrit.ChangeInput{
    81  		Project: "telemetry",
    82  		Subject: fmt.Sprintf("%s\n\nThis is an automated CL which updates the generated upload config.", clTitle),
    83  		Branch:  "master",
    84  	}
    85  	files := map[string]string{
    86  		"config/config.json": string(after),
    87  	}
    88  	return t.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, files)
    89  }
    90  
    91  // AwaitSubmission waits for the CL with the given change ID to be submitted.
    92  //
    93  // The return value is the submitted commit hash, or "" if changeID is "".
    94  func (t *TagTelemetryTasks) AwaitSubmission(ctx *wf.TaskContext, changeID string) error {
    95  	if changeID == "" {
    96  		ctx.Printf("not awaiting: no CL was created")
    97  		return nil
    98  	}
    99  
   100  	ctx.Printf("awaiting review/submit of %v", ChangeLink(changeID))
   101  	_, err := AwaitCondition(ctx, 10*time.Second, func() (string, bool, error) {
   102  		return t.Gerrit.Submitted(ctx, changeID, "")
   103  	})
   104  	return err
   105  }
   106  
   107  // MaybeTag tags x/telemetry/config with the next version if config/config.json
   108  // has changed.
   109  //
   110  // It returns the tag that was created, or "" if no tagging occurred.
   111  func (t *TagTelemetryTasks) MaybeTag(ctx *wf.TaskContext) (string, error) {
   112  	latestTag, latestVersion, err := t.latestConfigVersion(ctx)
   113  	if err != nil {
   114  		return "", err
   115  	}
   116  	if latestTag == "" {
   117  		ctx.Printf("not tagging: no existing release tag found, not tagging the initial version")
   118  		return "", nil
   119  	}
   120  	tagInfo, err := t.Gerrit.GetTag(ctx, "telemetry", latestTag)
   121  	if err != nil {
   122  		return "", fmt.Errorf("reading tag %s: %v", latestTag, err)
   123  	}
   124  
   125  	latestConfig, err := t.Gerrit.ReadFile(ctx, "telemetry", tagInfo.Revision, "config/config.json")
   126  	if err != nil {
   127  		return "", fmt.Errorf("reading config/config.json@latest: %v", err)
   128  	}
   129  	master, err := t.Gerrit.ReadBranchHead(ctx, "telemetry", "master")
   130  	if err != nil {
   131  		return "", fmt.Errorf("reading master commit: %v", err)
   132  	}
   133  	masterConfig, err := t.Gerrit.ReadFile(ctx, "telemetry", master, "config/config.json")
   134  	if err != nil {
   135  		return "", fmt.Errorf("reading config/config.json@master: %v", err)
   136  	}
   137  
   138  	if bytes.Equal(latestConfig, masterConfig) {
   139  		ctx.Printf("not tagging: no change to config.json since latest tag")
   140  		return "", nil
   141  	}
   142  
   143  	nextVer, err := nextMinor(latestVersion)
   144  	if err != nil {
   145  		return "", fmt.Errorf("couldn't pick next version: %v", err)
   146  	}
   147  	tag := "config/" + nextVer
   148  
   149  	ctx.Printf("tagging x/telemetry/config at %v as %v", master, tag)
   150  	if err := t.Gerrit.Tag(ctx, "telemetry", tag, master); err != nil {
   151  		return "", fmt.Errorf("failed to tag: %v", err)
   152  	}
   153  
   154  	return tag, nil
   155  }
   156  
   157  func (t *TagTelemetryTasks) latestConfigVersion(ctx context.Context) (tag, version string, _ error) {
   158  	tags, err := t.Gerrit.ListTags(ctx, "telemetry")
   159  	if err != nil {
   160  		return "", "", err
   161  	}
   162  	latestTag := ""
   163  	latestRelease := ""
   164  	for _, tag := range tags {
   165  		ver, ok := strings.CutPrefix(tag, "config/")
   166  		if !ok {
   167  			continue
   168  		}
   169  		if semver.IsValid(ver) && semver.Prerelease(ver) == "" &&
   170  			(latestRelease == "" || semver.Compare(latestRelease, ver) < 0) {
   171  			latestTag = tag
   172  			latestRelease = ver
   173  		}
   174  	}
   175  	return latestTag, latestRelease, nil
   176  }