cli: route `entire recap` to entire-api, team column included · Entire
cli: route entire recap to entire-api, team column included
2bf185a·
Soph·2w ago·5 files·+65 added/-29 removed
recap always sends the current repo as /me/recap's ?repo= to render the team/contributors column. entire-api addresses repos by ULID there, not owner/repo — but entire.io/api documents the mirror id as exactly that repo_id (repo_id = mirror_repos.id; "mirrorId is exactly the repoId entire-api wants"). The CLI already fetches mirrorId in the same listing it uses for the cell, so no extra lookup is needed.
- Carry the mirror id through as entireAPIPlacement.RepoID and expose it from the entire-api client as repoID.
- newRecapClient now returns the correct ?repo= value for its client: the repo ULID on entire-api, the owner/repo slug on the data API. It prefers the entire-api cell and, keeping recap's render-through-401 behaviour, falls back to the data API when not logged in.
Both personal and team recap work on entire-api; no owner/repo→ULID side-lookup required after all.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Sessions
43577009ee6cView transcript
Changes
5
cmd/entire/cli
Mentireapi_cell.go+9/-3
Mentireapi_cell_test.go+17/-12
Mentireapi_client.go+13/-5
Mentireapi_client_test.go+3/-3
Mrecap.go+23/-6
8 unmodified lines
9
10
11
12
13
12
13
14
15
16
17
18
19
20
21
22
23
24
25
24 unmodified lines
50
51
52
47
53
54
55
56
8 unmodified lines
// entireAPIPlacement is a repo's entire-api routing coordinates, read from its
// mirror: the cell that hosts it (fills {cell} in ENTIRE_API_BASE_URL_TEMPLATE)
// and the jurisdiction that homes it (fills {jurisdiction} in the audience and
// token-minting-core templates). Both must be present for a repo to be routable
// to entire-api; older cores omit them.
// token-minting-core templates). Cell and Jurisdiction must both be present for
// a repo to be routable to entire-api; older cores omit them.
//
// RepoID is the mirror id, which entire.io/api documents as exactly the repo_id
// entire-api uses for repo-scoped params (e.g. /me/recap?repo=<repo_id>) — not
// a separate identifier. It lets repo-scoped calls address the repo by ULID
// without a second lookup.
type entireAPIPlacement struct {
Cell string
Jurisdiction string
RepoID string
ClusterHost string
}
24 unmodified lines
if cell == "" || jur == "" {
continue
}
p := entireAPIPlacement{Cell: cell, Jurisdiction: jur, ClusterHost: mirrors[i].ClusterHost}
p := entireAPIPlacement{Cell: cell, Jurisdiction: jur, RepoID: mirrors[i].MirrorId, ClusterHost: mirrors[i].ClusterHost}
if preferredCluster != "" && strings.EqualFold(mirrors[i].ClusterHost, preferredCluster) {
return p, true
}
Mcmd/entire/cli/entireapi_cell.go+9/-3
6 unmodified lines
7
8
9
10
10
11
12
13
7 unmodified lines
21
22
23
24
25
26
27
28
29
24
25
26
27
28
29
30
31
32
32
33
34
35
36
33
34
35
36
37
38
39
40
41
58 unmodified lines
100
101
102
103
104
105
106
107
108
6 unmodified lines
)
func mirror(cluster, cell, jurisdiction string) coreapi.Mirror {
m := coreapi.Mirror{ClusterHost: cluster}
m := coreapi.Mirror{ClusterHost: cluster, MirrorId: cell + "-repo-ulid"}
if cell != "" {
m.Cell = coreapi.NewOptString(cell)
}
7 unmodified lines
t.Parallel()
tests := []struct {
name string
mirrors []coreapi.Mirror
preferred string
wantOK bool
wantCell string
wantJur string
name string
mirrors []coreapi.Mirror
preferred string
wantOK bool
wantCell string
wantJur string
wantRepoID string
}{
{
name: "single routable mirror",
mirrors: []coreapi.Mirror{mirror("a.entire.io", "aws-us-east-2", "us")},
wantOK: true,
wantCell: "aws-us-east-2",
wantJur: "us",
name: "single routable mirror",
mirrors: []coreapi.Mirror{mirror("a.entire.io", "aws-us-east-2", "us")},
wantOK: true,
wantCell: "aws-us-east-2",
wantJur: "us",
wantRepoID: "aws-us-east-2-repo-ulid",
},
{
name: "no mirrors",
58 unmodified lines
if got.Cell != tt.wantCell || got.Jurisdiction != tt.wantJur {
t.Fatalf("placement = %+v, want cell %q jurisdiction %q", got, tt.wantCell, tt.wantJur)
}
if tt.wantRepoID != "" && got.RepoID != tt.wantRepoID {
t.Fatalf("placement.RepoID = %q, want %q", got.RepoID, tt.wantRepoID)
}
})
}
}
Mcmd/entire/cli/entireapi_cell_test.go+17/-12
44 unmodified lines
45
46
47
48
48
49
50
51
52
53
54
55
56
57
58
59
2 unmodified lines
62
63
64
57
65
66
67
68
31 unmodified lines
100
101
102
95
103
104
105
106
1 unmodified line
108
109
110
103
111
112
113
114
115
116
117
110
118
119
44 unmodified lines
return placement, nil
}
// newEntireAPIClientForCurrentRepo builds an api.Client pointed at the entire-api
// entireAPIClient bundles an api.Client aimed at an entire-api cell with the
// current repo's ULID (RepoID), so repo-scoped params like /me/recap?repo=
// can address the repo by the id entire-api expects.
type entireAPIClient struct {
client *api.Client
repoID string
}
// newEntireAPIClientForCurrentRepo builds a client pointed at the entire-api
// cell hosting the current repo, carrying a jurisdictional identity token.
//
// ok=false with a nil error means "entire-api not in play": either the operator
2 unmodified lines
// data API. Genuine failures (control-plane unreachable, token exchange
// rejected, malformed template) return an error rather than silently masking a
// misconfigured opt-in.
func newEntireAPIClientForCurrentRepo(ctx context.Context, insecureHTTP bool) (*api.Client, bool, error) {
func newEntireAPIClientForCurrentRepo(ctx context.Context, insecureHTTP bool) (*entireAPIClient, bool, error) {
baseTemplate := api.EntireAPIBaseURLTemplate()
audTemplate := api.EntireAPIAudienceTemplate()
if baseTemplate == "" || audTemplate == "" {
31 unmodified lines
if err != nil {
return nil, false, fmt.Errorf("resolve entire-api token: %w", err)
}
return api.NewClientWithBaseURL(token, baseURL), true, nil
return &entireAPIClient{client: api.NewClientWithBaseURL(token, baseURL), repoID: placement.RepoID}, true, nil
}
// runAuthenticatedActivityAPI runs fn with an authenticated client for the
1 unmodified line
// configured and the repo is routable, otherwise the data API. Both serve the
// same /me/* paths, so fn is agnostic to which client it receives.
func runAuthenticatedActivityAPI(ctx context.Context, errW io.Writer, insecureHTTP bool, fn func(context.Context, *api.Client) error) error {
client, ok, err := newEntireAPIClientForCurrentRepo(ctx, insecureHTTP)
ec, ok, err := newEntireAPIClientForCurrentRepo(ctx, insecureHTTP)
if err != nil {
return renderDataAPIAuthError(errW, err)
}
if !ok {
return runAuthenticatedDataAPI(ctx, errW, insecureHTTP, fn)
}
return fn(ctx, client)
return fn(ctx, ec.client)
}
Mcmd/entire/cli/entireapi_client.go+13/-5
15 unmodified lines
16
17
18
19
19
20
21
22
23
24
23
24
25
26
27
15 unmodified lines
t.Setenv(api.EntireAPIBaseURLTemplateEnvVar, "")
t.Setenv(api.EntireAPIAudienceTemplateEnvVar, "")
client, ok, err := newEntireAPIClientForCurrentRepo(context.Background(), false)
ec, ok, err := newEntireAPIClientForCurrentRepo(context.Background(), false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ok || client != nil {
t.Fatalf("got (client=%v, ok=%v), want (nil, false) when unconfigured", client, ok)
if ok || ec != nil {
t.Fatalf("got (client=%v, ok=%v), want (nil, false) when unconfigured", ec, ok)
}
}
Mcmd/entire/cli/entireapi_client_test.go+3/-3
122 unmodified lines
123
124
125
126
126
127
128
129
8 unmodified lines
138
139
140
141
141
142
143
34 unmodified lines
178
179
180
182
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
3 unmodified lines
206
207
208
192
209
210
211
212
196
213
214
215
199
216
217
218
219
122 unmodified lines
if err != nil {
return err
}
client, err := newRecapClient(ctx, f.insecureHTTP)
client, repoSlug, err := newRecapClient(ctx, f.insecureHTTP)
if err != nil {
if errors.Is(err, api.ErrInsecureHTTP) {
fmt.Fprintf(errW, "ENTIRE_API_BASE_URL is set to an insecure http:// URL (%s). Use https:// for production, or pass --insecure-http-auth for local dev.\n", api.BaseURL())
8 unmodified lines
return err
}
rangeKey := f.rangeKey()
repoSlug := currentRepoSlug(ctx)
if f.useTUI(interactive.IsTerminalWriter(w), interactive.CanPromptInteractively(), IsAccessibleMode()) {
return runRecapTUI(ctx, client, recapTUIOptions{
Range: rangeKey,
34 unmodified lines
// these were all relabelled as keyring read failures via keyringReadError,
// which sent users on wild goose chases when the keyring was fine and the real
// problem was downstream.
func newRecapClient(ctx context.Context, insecureHTTP bool) (*api.Client, error) {
// newRecapClient returns the recap client and the value to pass as /me/recap's
// ?repo= (its team/contributors scope): the current repo's ULID when routed to
// entire-api (which addresses repos by id), or its owner/repo slug on the data
// API (which addresses them by name). Empty when the current repo can't be
// determined — recap then shows the personal side only.
//
// It prefers the entire-api cell for the current repo. recap tolerates not
// being logged in (it renders and lets the server answer 401), so an
// ErrNotLoggedIn from the entire-api attempt falls through to the data API
// rather than erroring; every other failure surfaces.
func newRecapClient(ctx context.Context, insecureHTTP bool) (*api.Client, string, error) {
ec, ok, err := newEntireAPIClientForCurrentRepo(ctx, insecureHTTP)
switch {
case err != nil && !errors.Is(err, auth.ErrNotLoggedIn):
return nil, "", err
case ok:
return ec.client, ec.repoID, nil
}
if insecureHTTP {
auth.EnableInsecureHTTP()
}
3 unmodified lines
err = nil
}
if err != nil {
return nil, err
return nil, "", err
}
if token != "" && !insecureHTTP {
if err := api.RequireSecureURL(api.BaseURL()); err != nil {
return nil, fmt.Errorf("base URL check: %w", err)
return nil, "", fmt.Errorf("base URL check: %w", err)
}
}
return api.NewClient(token), nil
return api.NewClient(token), currentRepoSlug(ctx), nil
}
func handleRecapFetchError(w io.Writer, err error) error {