|
| 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 | + "archive/tar" |
| 9 | + "bytes" |
| 10 | + "compress/gzip" |
| 11 | + "context" |
| 12 | + "fmt" |
| 13 | + "io" |
| 14 | + "path" |
| 15 | + "strings" |
| 16 | + "time" |
| 17 | + |
| 18 | + "golang.org/x/build/buildlet" |
| 19 | + "golang.org/x/build/gerrit" |
| 20 | + wf "golang.org/x/build/internal/workflow" |
| 21 | + "golang.org/x/mod/semver" |
| 22 | +) |
| 23 | + |
| 24 | +// TagTelemetryTasks implements a new workflow definition to tag |
| 25 | +// x/telemetry/config whenever the generated config.json changes. |
| 26 | +type TagTelemetryTasks struct { |
| 27 | + Gerrit GerritClient |
| 28 | + GerritURL string |
| 29 | + CreateBuildlet func(context.Context, string) (buildlet.RemoteClient, error) |
| 30 | + LatestGoBinaries func(context.Context) (string, error) |
| 31 | +} |
| 32 | + |
| 33 | +func (t *TagTelemetryTasks) NewDefinition() *wf.Definition { |
| 34 | + wd := wf.New() |
| 35 | + |
| 36 | + reviewers := wf.Param(wd, reviewersParam) |
| 37 | + changeID := wf.Task1(wd, "generate config CL", t.GenerateConfig, reviewers) |
| 38 | + submitted := wf.Action1(wd, "await config CL submission", t.AwaitSubmission, changeID) |
| 39 | + tag := wf.Task0(wd, "tag if appropriate", t.MaybeTag, wf.After(submitted)) |
| 40 | + wf.Output(wd, "tag", tag) |
| 41 | + |
| 42 | + return wd |
| 43 | +} |
| 44 | + |
| 45 | +// GenerateConfig runs the upload config generator in a buildlet, extracts the |
| 46 | +// resulting config.json, and creates a CL with the result if anything changed. |
| 47 | +// |
| 48 | +// It returns the change ID, or "" if the CL was not created. |
| 49 | +func (t *TagTelemetryTasks) GenerateConfig(ctx *wf.TaskContext, reviewers []string) (string, error) { |
| 50 | + const clTitle = "config: regenerate upload config" |
| 51 | + |
| 52 | + // Query for an existing pending config CL, to avoid duplication. |
| 53 | + // |
| 54 | + // Only wait a week, because configs are volatile: we really want to update |
| 55 | + // them within a week. |
| 56 | + query := fmt. Sprintf( `message:%q status:open owner:[email protected] repo:telemetry -age:7d`, clTitle) |
| 57 | + changes, err := t.Gerrit.QueryChanges(ctx, query) |
| 58 | + if err != nil { |
| 59 | + return "", err |
| 60 | + } |
| 61 | + if len(changes) > 0 { |
| 62 | + ctx.Printf("not creating CL: found existing CL %d", changes[0].ChangeNumber) |
| 63 | + return "", nil |
| 64 | + } |
| 65 | + |
| 66 | + binaries, err := t.LatestGoBinaries(ctx) |
| 67 | + if err != nil { |
| 68 | + return "", err |
| 69 | + } |
| 70 | + |
| 71 | + // linux-amd64 automatically disables outbound network access, unless explicitly specified by |
| 72 | + // setting GO_DISABLE_OUTBOUND_NETWORK=0. This has to be done every time Exec is called, since |
| 73 | + // once the network is disabled it cannot be undone. We could also use linux-amd64-longtest, |
| 74 | + // which does not have this property. |
| 75 | + bc, err := t.CreateBuildlet(ctx, "linux-amd64") |
| 76 | + if err != nil { |
| 77 | + return "", err |
| 78 | + } |
| 79 | + defer bc.Close() |
| 80 | + if err := bc.PutTarFromURL(ctx, binaries, ""); err != nil { |
| 81 | + return "", fmt.Errorf("putting Go binaries: %v", err) |
| 82 | + } |
| 83 | + tarURL := fmt.Sprintf("%s/%s/+archive/%s.tar.gz", t.GerritURL, "telemetry", "master") |
| 84 | + if err := bc.PutTarFromURL(ctx, tarURL, "telemetry"); err != nil { |
| 85 | + return "", fmt.Errorf("putting telemetry content: %v", err) |
| 86 | + } |
| 87 | + |
| 88 | + before, err := readBuildletFile(bc, "telemetry/config/config.json") |
| 89 | + if err != nil { |
| 90 | + return "", fmt.Errorf("reading initial config: %v", err) |
| 91 | + } |
| 92 | + |
| 93 | + logWriter := &LogWriter{Logger: ctx} |
| 94 | + go logWriter.Run(ctx) |
| 95 | + remoteErr, execErr := bc.Exec(ctx, "go/bin/go", buildlet.ExecOpts{ |
| 96 | + Dir: "telemetry", |
| 97 | + Args: []string{"run", "./internal/configgen", "-w"}, |
| 98 | + Output: logWriter, |
| 99 | + ExtraEnv: []string{"GO_DISABLE_OUTBOUND_NETWORK=0"}, |
| 100 | + }) |
| 101 | + if execErr != nil { |
| 102 | + return "", fmt.Errorf("Exec failed: %v", execErr) |
| 103 | + } |
| 104 | + if remoteErr != nil { |
| 105 | + return "", fmt.Errorf("Command failed: %v", remoteErr) |
| 106 | + } |
| 107 | + |
| 108 | + after, err := readBuildletFile(bc, "telemetry/config/config.json") |
| 109 | + if err != nil { |
| 110 | + return "", fmt.Errorf("reading generated config: %v", err) |
| 111 | + } |
| 112 | + |
| 113 | + if bytes.Equal(before, after) { |
| 114 | + ctx.Printf("not creating CL: config has not changed") |
| 115 | + return "", nil |
| 116 | + } |
| 117 | + |
| 118 | + changeInput := gerrit.ChangeInput{ |
| 119 | + Project: "telemetry", |
| 120 | + Subject: fmt.Sprintf("%s\n\nThis is an automated CL which updates the generated upload config.", clTitle), |
| 121 | + Branch: "master", |
| 122 | + } |
| 123 | + files := map[string]string{ |
| 124 | + "config/config.json": string(after), |
| 125 | + } |
| 126 | + return t.Gerrit.CreateAutoSubmitChange(ctx, changeInput, reviewers, files) |
| 127 | +} |
| 128 | + |
| 129 | +// readBuildletFile reads a single file from the buildlet at the specified file |
| 130 | +// path. |
| 131 | +func readBuildletFile(bc buildlet.RemoteClient, file string) ([]byte, error) { |
| 132 | + dir, name := path.Split(file) |
| 133 | + tgz, err := bc.GetTar(context.Background(), dir) |
| 134 | + if err != nil { |
| 135 | + return nil, err |
| 136 | + } |
| 137 | + defer tgz.Close() |
| 138 | + |
| 139 | + gzr, err := gzip.NewReader(tgz) |
| 140 | + if err != nil { |
| 141 | + return nil, err |
| 142 | + } |
| 143 | + defer gzr.Close() |
| 144 | + |
| 145 | + tr := tar.NewReader(gzr) |
| 146 | + for { |
| 147 | + h, err := tr.Next() |
| 148 | + if err == io.EOF { |
| 149 | + break |
| 150 | + } |
| 151 | + if err != nil { |
| 152 | + return nil, err |
| 153 | + } |
| 154 | + if h.Name == name && h.Typeflag == tar.TypeReg { |
| 155 | + return io.ReadAll(tr) |
| 156 | + } |
| 157 | + } |
| 158 | + return nil, fmt.Errorf("file %q not found", file) |
| 159 | +} |
| 160 | + |
| 161 | +// AwaitSubmitted waits for the CL with the given change ID to be submitted. |
| 162 | +// |
| 163 | +// The return value is the submitted commit hash, or "" if changeID is "". |
| 164 | +func (t *TagTelemetryTasks) AwaitSubmission(ctx *wf.TaskContext, changeID string) error { |
| 165 | + if changeID == "" { |
| 166 | + ctx.Printf("not awaiting: no CL was created") |
| 167 | + return nil |
| 168 | + } |
| 169 | + |
| 170 | + ctx.Printf("awaiting review/submit of %v", ChangeLink(changeID)) |
| 171 | + _, err := AwaitCondition(ctx, 10*time.Second, func() (string, bool, error) { |
| 172 | + return t.Gerrit.Submitted(ctx, changeID, "") |
| 173 | + }) |
| 174 | + return err |
| 175 | +} |
| 176 | + |
| 177 | +// MaybeTag tags x/telemetry/config with the next version if config/config.json |
| 178 | +// has changed. |
| 179 | +// |
| 180 | +// It returns the tag that was created, or "" if no tagging occurred. |
| 181 | +func (t *TagTelemetryTasks) MaybeTag(ctx *wf.TaskContext) (string, error) { |
| 182 | + latestTag, latestVersion, err := t.latestConfigVersion(ctx) |
| 183 | + if err != nil { |
| 184 | + return "", err |
| 185 | + } |
| 186 | + if latestTag == "" { |
| 187 | + ctx.Printf("not tagging: no existing release tag found, not tagging the initial version") |
| 188 | + return "", nil |
| 189 | + } |
| 190 | + |
| 191 | + latestConfig, err := t.Gerrit.ReadFile(ctx, "telemetry", latestTag, "config/config.json") |
| 192 | + if err != nil { |
| 193 | + return "", fmt.Errorf("reading config/config.json@latest: %v", err) |
| 194 | + } |
| 195 | + master, err := t.Gerrit.ReadBranchHead(ctx, "telemetry", "master") |
| 196 | + if err != nil { |
| 197 | + return "", fmt.Errorf("reading master commit: %v", err) |
| 198 | + } |
| 199 | + masterConfig, err := t.Gerrit.ReadFile(ctx, "telemetry", master, "config/config.json") |
| 200 | + if err != nil { |
| 201 | + return "", fmt.Errorf("reading config/config.json@master: %v", err) |
| 202 | + } |
| 203 | + |
| 204 | + if bytes.Equal(latestConfig, masterConfig) { |
| 205 | + ctx.Printf("not tagging: no change to config.json since latest tag") |
| 206 | + return "", nil |
| 207 | + } |
| 208 | + |
| 209 | + nextVer, err := nextMinor(latestVersion) |
| 210 | + if err != nil { |
| 211 | + return "", fmt.Errorf("couldn't pick next version: %v", err) |
| 212 | + } |
| 213 | + tag := "config/" + nextVer |
| 214 | + |
| 215 | + ctx.Printf("tagging x/telemetry/config at %v as %v", master, tag) |
| 216 | + if err := t.Gerrit.Tag(ctx, "telemetry", tag, master); err != nil { |
| 217 | + return "", fmt.Errorf("failed to tag: %v", err) |
| 218 | + } |
| 219 | + |
| 220 | + return tag, nil |
| 221 | +} |
| 222 | + |
| 223 | +func (t *TagTelemetryTasks) latestConfigVersion(ctx context.Context) (tag, version string, _ error) { |
| 224 | + tags, err := t.Gerrit.ListTags(ctx, "telemetry") |
| 225 | + if err != nil { |
| 226 | + return "", "", err |
| 227 | + } |
| 228 | + latestTag := "" |
| 229 | + latestRelease := "" |
| 230 | + for _, tag := range tags { |
| 231 | + ver, ok := strings.CutPrefix(tag, "config/") |
| 232 | + if !ok { |
| 233 | + continue |
| 234 | + } |
| 235 | + if semver.IsValid(ver) && semver.Prerelease(ver) == "" && |
| 236 | + (latestRelease == "" || semver.Compare(latestRelease, ver) < 0) { |
| 237 | + latestTag = tag |
| 238 | + latestRelease = ver |
| 239 | + } |
| 240 | + } |
| 241 | + return latestTag, latestRelease, nil |
| 242 | +} |
0 commit comments