diff --git a/cmd/relui/main.go b/cmd/relui/main.go index 1aaf6b6244..dc6ecba16c 100644 --- a/cmd/relui/main.go +++ b/cmd/relui/main.go @@ -215,6 +215,7 @@ func main() { ApproveAction: relui.ApproveActionDep(dbPool), } dh.RegisterDefinition("Tag x/ repos", tagTasks.NewDefinition()) + dh.RegisterDefinition("Tag a single x/ repo", tagTasks.NewSingleDefinition()) var base *url.URL if *baseURL != "" { diff --git a/internal/task/tagx.go b/internal/task/tagx.go index 13d9888e29..a0cfb5eb37 100644 --- a/internal/task/tagx.go +++ b/internal/task/tagx.go @@ -45,13 +45,22 @@ func (x *TagXReposTasks) NewDefinition() *wf.Definition { return wd } +func (x *TagXReposTasks) NewSingleDefinition() *wf.Definition { + wd := wf.New() + repos := wf.Task0(wd, "Load all repositories", x.SelectRepos) + name := wf.Param(wd, wf.ParamDef[string]{Name: "Repository name", Example: "tools"}) + wf.Expand2(wd, "Create single-repo plan", x.BuildSingleRepoPlan, repos, name) + return wd +} + // TagRepo contains information about a repo that can be tagged. type TagRepo struct { - Name string // Gerrit project name, e.g. "tools". - ModPath string // Module path, e.g. "golang.org/x/tools". - Deps []string // Dependency module paths. - Compat string // The version to pass to go mod tidy -compat for this repository. - Version string // After a tagging decision has been made, the version dependencies should upgrade to. + Name string // Gerrit project name, e.g. "tools". + ModPath string // Module path, e.g. "golang.org/x/tools". + Deps []string // Dependency module paths. + Compat string // The Go version to pass to go mod tidy -compat for this repository. + StartVersion string // The version of the module when the workflow started. + Version string // After a tagging decision has been made, the version dependencies should upgrade to. } func (x *TagXReposTasks) SelectRepos(ctx *wf.TaskContext) ([]TagRepo, error) { @@ -127,8 +136,9 @@ func (x *TagXReposTasks) readRepo(ctx *wf.TaskContext, project string) (*TagRepo } result := &TagRepo{ - Name: project, - ModPath: mf.Module.Mod.Path, + Name: project, + ModPath: mf.Module.Mod.Path, + StartVersion: tag, } compatRe := regexp.MustCompile(`tagx:compat\s+([\d.]+)`) @@ -251,8 +261,31 @@ func (x *TagXReposTasks) BuildPlan(wd *wf.Definition, repos []TagRepo) error { return nil } -// planRepo returns a Value containing the tagged repository's information, or -// nil, false if its dependencies haven't been planned yet. +func (x *TagXReposTasks) BuildSingleRepoPlan(wd *wf.Definition, repoSlice []TagRepo, name string) error { + repos := map[string]TagRepo{} + updatedRepos := map[string]wf.Value[TagRepo]{} + for _, r := range repoSlice { + repos[r.Name] = r + + // Pretend that we've just tagged version that was live when we started. + r.Version = r.StartVersion + updatedRepos[r.ModPath] = wf.Const(r) + } + repo, ok := repos[name] + if !ok { + return fmt.Errorf("no repository %q", name) + } + tagged, ok := x.planRepo(wd, repo, updatedRepos) + if !ok { + return fmt.Errorf("%q doesn't have all of its dependencies (%q)", repo.Name, repo.Deps) + } + wf.Output(wd, "tagged repository", tagged) + return nil +} + +// planRepo adds tasks to wf to update and tag repo. It returns a Value +// containing the tagged repository's information, or nil, false if its +// dependencies haven't been planned yet. func (x *TagXReposTasks) planRepo(wd *wf.Definition, repo TagRepo, updated map[string]wf.Value[TagRepo]) (_ wf.Value[TagRepo], ready bool) { var deps []wf.Value[TagRepo] for _, repoDeps := range repo.Deps { diff --git a/internal/task/tagx_test.go b/internal/task/tagx_test.go index 24ceb8eb65..77a527e19d 100644 --- a/internal/task/tagx_test.go +++ b/internal/task/tagx_test.go @@ -334,11 +334,20 @@ case "$1" in esac ` -func TestTagXRepos(t *testing.T) { +type tagXTestDeps struct { + ctx context.Context + gerrit *FakeGerrit + tagXTasks *TagXReposTasks +} + +func newTagXTestDeps(t *testing.T, repos ...*FakeRepo) *tagXTestDeps { if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { t.Skip("Requires bash shell scripting support.") } + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + goServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ServeTarball("dl/go1.19.linux-amd64.tar.gz", map[string]string{ "go/bin/go": fakeGo, @@ -346,36 +355,14 @@ func TestTagXRepos(t *testing.T) { })) t.Cleanup(goServer.Close) - fakeBuildlets := NewFakeBuildlets(t, "") - goRepo := NewFakeRepo(t, "go") go1 := goRepo.Commit(map[string]string{ "main.go": "I'm the go command or something", }) - sys := NewFakeRepo(t, "sys") - sys1 := sys.Commit(map[string]string{ - "go.mod": "module golang.org/x/sys\n", - "go.sum": "\n", - }) - sys.Tag("v0.1.0", sys1) - sys2 := sys.Commit(map[string]string{ - "main.go": "package main", - }) - mod := NewFakeRepo(t, "mod") - mod1 := mod.Commit(map[string]string{ - "go.mod": "module golang.org/x/mod\n", - "go.sum": "\n", - }) - mod.Tag("v1.0.0", mod1) - tools := NewFakeRepo(t, "tools") - tools1 := tools.Commit(map[string]string{ - "go.mod": "module golang.org/x/tools\nrequire golang.org/x/mod v1.0.0\ngo 1.18 // tagx:compat 1.16\nrequire golang.org/x/sys v0.1.0\n", - "go.sum": "\n", - "gopls/go.mod": "module golang.org/x/tools/gopls\nrequire golang.org/x/mod v1.0.0\n", - "gopls/go.sum": "\n", - }) - tools.Tag("v1.1.5", tools1) - fakeGerrit := NewFakeGerrit(t, goRepo, sys, mod, tools) + repos = append(repos, goRepo) + + fakeBuildlets := NewFakeBuildlets(t, "") + fakeGerrit := NewFakeGerrit(t, repos...) var builders, allOK []string for _, b := range dashboard.Builders { builders = append(builders, b.Name) @@ -391,7 +378,7 @@ func TestTagXRepos(t *testing.T) { }, } } - for _, r := range []*FakeRepo{sys, mod, tools} { + for _, r := range repos { if repo != "golang.org/x/"+r.name { continue } @@ -427,19 +414,53 @@ func TestTagXRepos(t *testing.T) { return nil }, } - wd := tasks.NewDefinition() + return &tagXTestDeps{ + ctx: ctx, + gerrit: fakeGerrit, + tagXTasks: tasks, + } +} + +func TestTagXRepos(t *testing.T) { + sys := NewFakeRepo(t, "sys") + sys1 := sys.Commit(map[string]string{ + "go.mod": "module golang.org/x/sys\n", + "go.sum": "\n", + }) + sys.Tag("v0.1.0", sys1) + sys2 := sys.Commit(map[string]string{ + "main.go": "package main", + }) + mod := NewFakeRepo(t, "mod") + mod1 := mod.Commit(map[string]string{ + "go.mod": "module golang.org/x/mod\n", + "go.sum": "\n", + }) + mod.Tag("v1.0.0", mod1) + tools := NewFakeRepo(t, "tools") + tools1 := tools.Commit(map[string]string{ + "go.mod": "module golang.org/x/tools\nrequire golang.org/x/mod v1.0.0\ngo 1.18 // tagx:compat 1.16\nrequire golang.org/x/sys v0.1.0\n", + "go.sum": "\n", + "gopls/go.mod": "module golang.org/x/tools/gopls\nrequire golang.org/x/mod v1.0.0\n", + "gopls/go.sum": "\n", + }) + tools.Tag("v1.1.5", tools1) + + deps := newTagXTestDeps(t, sys, mod, tools) + + wd := deps.tagXTasks.NewDefinition() w, err := workflow.Start(wd, nil) if err != nil { t.Fatal(err) } - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(deps.ctx, time.Minute) defer cancel() _, err = w.Run(ctx, &verboseListener{t: t}) if err != nil { t.Fatal(err) } - tag, err := fakeGerrit.GetTag(ctx, "sys", "v0.2.0") + tag, err := deps.gerrit.GetTag(ctx, "sys", "v0.2.0") if err != nil { t.Fatalf("sys should have been tagged with v0.2.0: %v", err) } @@ -447,7 +468,7 @@ func TestTagXRepos(t *testing.T) { t.Errorf("sys v0.2.0 = %v, want %v", tag.Revision, sys2) } - tags, err := fakeGerrit.ListTags(ctx, "mod") + tags, err := deps.gerrit.ListTags(ctx, "mod") if err != nil { t.Fatal(err) } @@ -455,11 +476,11 @@ func TestTagXRepos(t *testing.T) { t.Errorf("mod has tags %v, wanted only v1.0.0", tags) } - tag, err = fakeGerrit.GetTag(ctx, "tools", "v1.2.0") + tag, err = deps.gerrit.GetTag(ctx, "tools", "v1.2.0") if err != nil { t.Fatalf("tools should have been tagged with v1.2.0: %v", err) } - goMod, err := fakeGerrit.ReadFile(ctx, "tools", tag.Revision, "go.mod") + goMod, err := deps.gerrit.ReadFile(ctx, "tools", tag.Revision, "go.mod") if err != nil { t.Fatal(err) } @@ -469,7 +490,7 @@ func TestTagXRepos(t *testing.T) { if !strings.Contains(string(goMod), "tidied!") { t.Error("tools go.mod should be tidied") } - goplsMod, err := fakeGerrit.ReadFile(ctx, "tools", tag.Revision, "gopls/go.mod") + goplsMod, err := deps.gerrit.ReadFile(ctx, "tools", tag.Revision, "gopls/go.mod") if err != nil { t.Fatal(err) } @@ -478,6 +499,52 @@ func TestTagXRepos(t *testing.T) { } } +func TestTagSingleRepo(t *testing.T) { + mod := NewFakeRepo(t, "mod") + mod1 := mod.Commit(map[string]string{ + "go.mod": "module golang.org/x/mod\n", + "go.sum": "\n", + }) + mod.Tag("v1.1.0", mod1) + foo := NewFakeRepo(t, "foo") + foo1 := foo.Commit(map[string]string{ + "go.mod": "module golang.org/x/foo\nrequire golang.org/x/mod v1.0.0\n", + "go.sum": "\n", + }) + foo.Tag("v1.1.5", foo1) + foo.Commit(map[string]string{ + "main.go": "package main", + }) + + deps := newTagXTestDeps(t, mod, foo) + + wd := deps.tagXTasks.NewSingleDefinition() + ctx, cancel := context.WithTimeout(deps.ctx, time.Minute) + w, err := workflow.Start(wd, map[string]interface{}{ + "Repository name": "foo", + }) + if err != nil { + t.Fatal(err) + } + defer cancel() + _, err = w.Run(ctx, &verboseListener{t: t}) + if err != nil { + t.Fatal(err) + } + + tag, err := deps.gerrit.GetTag(ctx, "foo", "v1.2.0") + if err != nil { + t.Fatalf("foo should have been tagged with v1.2.0: %v", err) + } + goMod, err := deps.gerrit.ReadFile(ctx, "foo", tag.Revision, "go.mod") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(goMod), "mod@v1.1.0") { + t.Errorf("foo should use mod v1.1.0. go.mod: %v", string(goMod)) + } +} + type verboseListener struct { t *testing.T outputListener func(string, interface{}) diff --git a/internal/workflow/workflow.go b/internal/workflow/workflow.go index 93c16e1aed..1a10f1511b 100644 --- a/internal/workflow/workflow.go +++ b/internal/workflow/workflow.go @@ -83,6 +83,7 @@ func (d *Definition) name(name string) string { func (d *Definition) shallowClone() *Definition { clone := New() + clone.parameters = append([]MetaParameter(nil), d.parameters...) for k, v := range d.tasks { clone.tasks[k] = v }