Skip to content

Commit 4999efa

Browse files
committed
internal/task, cmd/relui: add definitions for tagging x/telemetry/config
Add a new workflow definition to automate the updating and tagging of the x/telemetry/config module. Change-Id: I5bbf46f88b025587ffba6e0c6960e1e22f04139e Reviewed-on: https://go-review.googlesource.com/c/build/+/522178 TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]>
1 parent c21d7be commit 4999efa

File tree

4 files changed

+396
-1
lines changed

4 files changed

+396
-1
lines changed

cmd/relui/main.go

+8
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,14 @@ func main() {
237237
}
238238
dh.RegisterDefinition("Update x/crypto NSS root bundle", bundleTasks.NewDefinition())
239239

240+
tagTelemetryTasks := &task.TagTelemetryTasks{
241+
Gerrit: gerritClient,
242+
GerritURL: "https://go.googlesource.com",
243+
CreateBuildlet: coordinator.CreateBuildlet,
244+
LatestGoBinaries: task.LatestGoBinaries,
245+
}
246+
dh.RegisterDefinition("Tag a new version of x/telemetry/config (if necessary)", tagTelemetryTasks.NewDefinition())
247+
240248
var base *url.URL
241249
if *baseURL != "" {
242250
base, err = url.Parse(*baseURL)

internal/task/fakes.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ func NewFakeRepo(t *testing.T, name string) *FakeRepo {
370370
return r
371371
}
372372

373+
// TODO(rfindley): probably every method on FakeRepo should invoke
374+
// repo.t.Helper(), otherwise it's impossible to see where the test failed.
375+
373376
func (repo *FakeRepo) runGit(args ...string) []byte {
374377
repo.t.Helper()
375378
configArgs := []string{
@@ -379,7 +382,7 @@ func (repo *FakeRepo) runGit(args ...string) []byte {
379382
}
380383
out, err := repo.dir.RunCommand(context.Background(), append(configArgs, args...)...)
381384
if err != nil {
382-
repo.t.Fatal(err)
385+
repo.t.Fatalf("runGit(%v) failed: %v; output:\n%s", args, err, out)
383386
}
384387
return out
385388
}

internal/task/tagtelemetry.go

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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

Comments
 (0)