diff --git a/internal/tools/create_project_from_template.go b/internal/tools/create_project_from_template.go index ad94569..37619a1 100644 --- a/internal/tools/create_project_from_template.go +++ b/internal/tools/create_project_from_template.go @@ -151,12 +151,6 @@ func (t *CreateProjectFromTemplate) Call(ctx context.Context, raw json.RawMessag } result.DefaultBranch = branch - // Wait for the generated branch to become writable. gitea's /generate is - // async: it returns (and serves reads) before the branch ref is committed, so - // the first writes 404 "branch does not exist" for a beat. Poll until the - // branch is present. - t.waitForBranch(ctx, args.Owner, args.Name, branch) - // Substitute across the WHOLE tree: content in every blob, plus a path rename // for any file whose path carries a placeholder (e.g. cmd/__PROJECT_NAME__/main.go). // A fixed known-files list can't rename directories or cover every templated @@ -192,27 +186,16 @@ func (t *CreateProjectFromTemplate) Call(ctx context.Context, raw json.RawMessag return textOK(result) } -// waitForBranch polls until the freshly-generated branch is present, or a short -// budget elapses. Best-effort — upsertRetry covers the remaining read-vs-write -// consistency window where the branch reads but does not yet write. -func (t *CreateProjectFromTemplate) waitForBranch(ctx context.Context, owner, name, branch string) { - for i := 0; i < 20; i++ { - if ok, err := t.c.BranchExists(ctx, owner, name, branch); ok && err == nil { - return - } - select { - case <-ctx.Done(): - return - case <-time.After(500 * time.Millisecond): - } - } -} - -// upsertRetry retries UpsertFile through the brief post-generate window where the -// branch is readable but writes still 404 "branch does not exist". +// upsertRetry is the readiness gate for the async-generate branch race: gitea's +// /generate returns (and serves reads) before the branch ref is committed, so the +// first writes 404 "branch does not exist" until the initial commit lands (observed +// up to ~30s under load). BranchExists is not a usable signal — it reports the +// branch present before writes succeed. So the write itself is the probe: retry on +// the transient not-found until it takes. Once the first write lands, the branch is +// writable and the rest succeed on the first try. func (t *CreateProjectFromTemplate) upsertRetry(ctx context.Context, owner, name, path string, args gitea.UpsertFileArgs) error { var err error - for i := 0; i < 5; i++ { + for i := 0; i < 60; i++ { if _, err = t.c.UpsertFile(ctx, owner, name, path, args); err == nil { return nil } @@ -222,7 +205,7 @@ func (t *CreateProjectFromTemplate) upsertRetry(ctx context.Context, owner, name select { case <-ctx.Done(): return err - case <-time.After(500 * time.Millisecond): + case <-time.After(time.Second): } } return err diff --git a/internal/tools/create_project_from_template_test.go b/internal/tools/create_project_from_template_test.go index 4cc81f8..a91f744 100644 --- a/internal/tools/create_project_from_template_test.go +++ b/internal/tools/create_project_from_template_test.go @@ -69,10 +69,6 @@ func (f *fakeTemplateServer) handler(t *testing.T, tmpl, dest string) http.Handl _, _ = fmt.Fprintf(w, `{"name":%q,"full_name":"mathias/%s","default_branch":%q,"clone_url":"http://gitea.example.com/mathias/%s.git","html_url":"http://gitea.example.com/mathias/%s","template":false}`, dest, dest, f.genBranch, dest, dest) - case r.Method == http.MethodGet && strings.HasPrefix(p, "/api/v1/repos/mathias/"+dest+"/branches/"): - // waitForBranch readiness probe — branch is present. - _, _ = fmt.Fprint(w, `{"name":"main","commit":{"id":"c"}}`) - case r.Method == http.MethodGet && strings.HasPrefix(p, "/api/v1/repos/mathias/"+dest+"/git/trees/"): var entries []string for path := range f.files {