diff --git a/internal/setup/gitlab.go b/internal/setup/gitlab.go index c078db5..d7be672 100644 --- a/internal/setup/gitlab.go +++ b/internal/setup/gitlab.go @@ -1187,12 +1187,22 @@ func (g *gitlabSetup) defaultBranch(ctx context.Context) { g.r.add(step, StatusWould, "would protect %s: push No one, merge Maintainers, no force push", b) return } - if err := g.send(ctx, http.MethodPost, protect, nil, "projects", id(g.hub.ID), "protected_branches"); err != nil { - g.r.add(step, StatusFail, "cannot protect %s: %v", b, err) + perr := g.send(ctx, http.MethodPost, protect, nil, "projects", id(g.hub.ID), "protected_branches") + if perr == nil { + g.r.add(step, StatusDone, "protected %s: push No one, merge Maintainers, no force push", b) + return + } + // GitLab protects a new project's default branch from a background + // job: the read above may miss a protection the POST then meets + // (409). Read it again, and go on as for a branch found protected. + if !isStatus(perr, http.StatusConflict) { + g.r.add(step, StatusFail, "cannot protect %s: %v", b, perr) + return + } + if err := g.get(ctx, &pb, "projects", id(g.hub.ID), "protected_branches", b); err != nil { + g.r.add(step, StatusFail, "cannot protect %s: %v; and reading the protection GitLab reports: %v", b, perr, err) return } - g.r.add(step, StatusDone, "protected %s: push No one, merge Maintainers, no force push", b) - return } if pushOK(pb.Push) && mergeOK(pb.Merge) && !pb.AllowForcePush { g.r.add(step, StatusOK, "%s is protected: push No one, merge %s, no force push", b, mergeLevels(pb.Merge)) diff --git a/internal/setup/gitlab_test.go b/internal/setup/gitlab_test.go index 55197af..fd6c856 100644 --- a/internal/setup/gitlab_test.go +++ b/internal/setup/gitlab_test.go @@ -709,3 +709,16 @@ func TestGitLabWriterOnHub(t *testing.T) { t.Errorf("writer-guard %s %q, exit %d", s.Status, s.Detail, rep.ExitCode()) } } + +// TestGitLabLateProtection: the read of main misses the protection GitLab +// is still making for the new project, and the POST meets it (409): +// setup reads it again and protects main as for a branch found protected. +func TestGitLabLateProtection(t *testing.T) { + w := newGLWorld(t, "18.11") + w.f.lateProtection = true + rep := w.run(t, w.input(false)) + if s := step(t, rep, "default-branch"); s.Status != StatusDone { + t.Errorf("default-branch %s %q", s.Status, s.Detail) + } + w.checkHub(t, rep) +} diff --git a/internal/setup/glfake_test.go b/internal/setup/glfake_test.go index dd509b4..e730e6f 100644 --- a/internal/setup/glfake_test.go +++ b/internal/setup/glfake_test.go @@ -41,18 +41,22 @@ type glFake struct { t *testing.T srv *httptest.Server - mu sync.Mutex - version string - saAPI bool - saCreate bool - premium bool - next int64 - users map[int64]*fUser - tokens map[string]*fToken - groups map[int64]*fGroup - projects map[int64]*fProject - writes []string // "METHOD path" of every request that is not a GET - failRoutes map[string]int + mu sync.Mutex + version string + saAPI bool + saCreate bool + premium bool + // lateProtection makes the first read of a protected branch answer 404 + // although it exists: GitLab protects a new project's default branch + // from a background job, which a read may not see yet. + lateProtection bool + next int64 + users map[int64]*fUser + tokens map[string]*fToken + groups map[int64]*fGroup + projects map[int64]*fProject + writes []string // "METHOD path" of every request that is not a GET + failRoutes map[string]int } type fUser struct { @@ -766,6 +770,10 @@ func (f *glFake) serveProject(w http.ResponseWriter, r *http.Request, route stri notFound(w) case "GET projects/:projects/protected_branches/:protected_branches": b := p.branches[s[3]] + if f.lateProtection { + f.lateProtection = false + b = nil + } if b == nil { notFound(w) return