Monorepo for Tangled tangled.org
856
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview,knotmirror: tolerate 1.13 knots during 1.14 rollout #350

open opened by oyster.cafe targeting master from lt/repo-rename-by-rkey
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mldmliftai22
+327 -24
Diff #1
+87
appview/compat113/compat.go
··· 1 + package compat113 2 + 3 + import ( 4 + "encoding/json" 5 + "io" 6 + 7 + lexutil "github.com/bluesky-social/indigo/lex/util" 8 + "tangled.org/core/api/tangled" 9 + ) 10 + 11 + func Collaborator(r *tangled.RepoCollaborator) *lexutil.LexiconTypeDecoder { 12 + return &lexutil.LexiconTypeDecoder{Val: &collaboratorWrapper{inner: r}} 13 + } 14 + 15 + func Pull(r *tangled.RepoPull) *lexutil.LexiconTypeDecoder { 16 + return &lexutil.LexiconTypeDecoder{Val: &pullWrapper{inner: r}} 17 + } 18 + 19 + type collaboratorWrapper struct { 20 + LexiconTypeID string `cborgen:"$type,const=sh.tangled.repo.collaborator"` 21 + inner *tangled.RepoCollaborator 22 + } 23 + 24 + func (c *collaboratorWrapper) MarshalJSON() ([]byte, error) { 25 + c.inner.LexiconTypeID = "sh.tangled.repo.collaborator" 26 + return marshalWithRepoDidShadow(c.inner, false) 27 + } 28 + 29 + func (c *collaboratorWrapper) MarshalCBOR(w io.Writer) error { 30 + return c.inner.MarshalCBOR(w) 31 + } 32 + 33 + type pullWrapper struct { 34 + LexiconTypeID string `cborgen:"$type,const=sh.tangled.repo.pull"` 35 + inner *tangled.RepoPull 36 + } 37 + 38 + func (c *pullWrapper) MarshalJSON() ([]byte, error) { 39 + c.inner.LexiconTypeID = "sh.tangled.repo.pull" 40 + return marshalWithRepoDidShadow(c.inner, true) 41 + } 42 + 43 + func (c *pullWrapper) MarshalCBOR(w io.Writer) error { 44 + return c.inner.MarshalCBOR(w) 45 + } 46 + 47 + func marshalWithRepoDidShadow(inner any, nestedTarget bool) ([]byte, error) { 48 + raw, err := json.Marshal(inner) 49 + if err != nil { 50 + return nil, err 51 + } 52 + var top map[string]json.RawMessage 53 + if err := json.Unmarshal(raw, &top); err != nil { 54 + return raw, nil 55 + } 56 + if nestedTarget { 57 + injectIntoNested(top, "target") 58 + injectIntoNested(top, "source") 59 + } else { 60 + addRepoDidShadow(top) 61 + } 62 + return json.Marshal(top) 63 + } 64 + 65 + func injectIntoNested(parent map[string]json.RawMessage, key string) { 66 + raw, ok := parent[key] 67 + if !ok { 68 + return 69 + } 70 + var nested map[string]json.RawMessage 71 + if err := json.Unmarshal(raw, &nested); err != nil { 72 + return 73 + } 74 + addRepoDidShadow(nested) 75 + if reb, err := json.Marshal(nested); err == nil { 76 + parent[key] = reb 77 + } 78 + } 79 + 80 + func addRepoDidShadow(m map[string]json.RawMessage) { 81 + if _, has := m["repoDid"]; has { 82 + return 83 + } 84 + if v, ok := m["repo"]; ok { 85 + m["repoDid"] = v 86 + } 87 + }
+113
appview/compat113/compat_test.go
··· 1 + package compat113 2 + 3 + import ( 4 + "encoding/json" 5 + "testing" 6 + 7 + "tangled.org/core/api/tangled" 8 + ) 9 + 10 + func ptr[T any](v T) *T { return &v } 11 + 12 + func TestCollaboratorShadowsRepoDid(t *testing.T) { 13 + rec := &tangled.RepoCollaborator{ 14 + CreatedAt: "2026-05-08T00:00:00Z", 15 + Repo: "did:plc:abalone", 16 + Subject: "did:plc:limpet", 17 + } 18 + 19 + out, err := json.Marshal(Collaborator(rec)) 20 + if err != nil { 21 + t.Fatalf("marshal: %v", err) 22 + } 23 + 24 + var got map[string]any 25 + if err := json.Unmarshal(out, &got); err != nil { 26 + t.Fatalf("unmarshal: %v", err) 27 + } 28 + 29 + if got["$type"] != "sh.tangled.repo.collaborator" { 30 + t.Errorf("$type = %v, want sh.tangled.repo.collaborator", got["$type"]) 31 + } 32 + if got["repo"] != "did:plc:abalone" { 33 + t.Errorf("repo = %v, want did:plc:abalone", got["repo"]) 34 + } 35 + if got["repoDid"] != "did:plc:abalone" { 36 + t.Errorf("repoDid shadow missing or wrong: got %v", got["repoDid"]) 37 + } 38 + } 39 + 40 + func TestPullShadowsTargetRepoDid(t *testing.T) { 41 + rec := &tangled.RepoPull{ 42 + CreatedAt: "2026-05-08T00:00:00Z", 43 + Title: "rename whelk handler", 44 + Target: &tangled.RepoPull_Target{ 45 + Branch: "main", 46 + Repo: "did:plc:scallop", 47 + }, 48 + Source: &tangled.RepoPull_Source{ 49 + Branch: "feature-1", 50 + }, 51 + } 52 + 53 + out, err := json.Marshal(Pull(rec)) 54 + if err != nil { 55 + t.Fatalf("marshal: %v", err) 56 + } 57 + 58 + var got map[string]any 59 + if err := json.Unmarshal(out, &got); err != nil { 60 + t.Fatalf("unmarshal: %v", err) 61 + } 62 + 63 + target, ok := got["target"].(map[string]any) 64 + if !ok { 65 + t.Fatalf("target missing or wrong type: %v", got["target"]) 66 + } 67 + if target["repo"] != "did:plc:scallop" { 68 + t.Errorf("target.repo = %v", target["repo"]) 69 + } 70 + if target["repoDid"] != "did:plc:scallop" { 71 + t.Errorf("target.repoDid shadow missing: %v", target["repoDid"]) 72 + } 73 + 74 + if _, has := got["repoDid"]; has { 75 + t.Errorf("top-level repoDid should not be set on pull: %v", got["repoDid"]) 76 + } 77 + } 78 + 79 + func TestPullShadowsForkSourceRepoDid(t *testing.T) { 80 + rec := &tangled.RepoPull{ 81 + CreatedAt: "2026-05-08T00:00:00Z", 82 + Title: "fork-based PR", 83 + Target: &tangled.RepoPull_Target{ 84 + Branch: "main", 85 + Repo: "did:plc:scallop", 86 + }, 87 + Source: &tangled.RepoPull_Source{ 88 + Branch: "feature-2", 89 + Repo: ptr("did:plc:periwinkle"), 90 + }, 91 + } 92 + 93 + out, err := json.Marshal(Pull(rec)) 94 + if err != nil { 95 + t.Fatalf("marshal: %v", err) 96 + } 97 + 98 + var got map[string]any 99 + if err := json.Unmarshal(out, &got); err != nil { 100 + t.Fatalf("unmarshal: %v", err) 101 + } 102 + 103 + source, ok := got["source"].(map[string]any) 104 + if !ok { 105 + t.Fatalf("source missing: %v", got["source"]) 106 + } 107 + if source["repo"] != "did:plc:periwinkle" { 108 + t.Errorf("source.repo = %v", source["repo"]) 109 + } 110 + if source["repoDid"] != "did:plc:periwinkle" { 111 + t.Errorf("source.repoDid shadow missing: %v", source["repoDid"]) 112 + } 113 + }
+60
appview/compat113/version.go
··· 1 + package compat113 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/http" 7 + "strconv" 8 + "strings" 9 + "time" 10 + 11 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 12 + "tangled.org/core/api/tangled" 13 + ) 14 + 15 + const versionProbeTimeout = 5 * time.Second 16 + 17 + func KnotSupports114(ctx context.Context, host string, dev bool) bool { 18 + scheme := "https" 19 + if dev { 20 + scheme = "http" 21 + } 22 + client := &indigoxrpc.Client{ 23 + Host: fmt.Sprintf("%s://%s", scheme, host), 24 + Client: &http.Client{Timeout: versionProbeTimeout}, 25 + } 26 + 27 + ctx, cancel := context.WithTimeout(ctx, versionProbeTimeout) 28 + defer cancel() 29 + 30 + resp, err := tangled.KnotVersion(ctx, client) 31 + if err != nil || resp == nil { 32 + return true 33 + } 34 + return atLeast114(resp.Version) 35 + } 36 + 37 + func atLeast114(v string) bool { 38 + v = strings.TrimSpace(v) 39 + v = strings.TrimPrefix(v, "v") 40 + if strings.HasPrefix(v, "(devel)") { 41 + return true 42 + } 43 + if v == "" { 44 + return false 45 + } 46 + parts := strings.SplitN(v, ".", 3) 47 + if len(parts) < 2 { 48 + return false 49 + } 50 + major, err := strconv.Atoi(parts[0]) 51 + if err != nil { 52 + return false 53 + } 54 + minorRaw := strings.SplitN(parts[1], "-", 2)[0] 55 + minor, err := strconv.Atoi(minorRaw) 56 + if err != nil { 57 + return false 58 + } 59 + return major > 1 || (major == 1 && minor >= 14) 60 + }
+35
appview/compat113/version_test.go
··· 1 + package compat113 2 + 3 + import "testing" 4 + 5 + func TestAtLeast114(t *testing.T) { 6 + cases := []struct { 7 + in string 8 + want bool 9 + }{ 10 + {"v1.14.0", true}, 11 + {"v1.14.0-alpha", true}, 12 + {"v1.14.5", true}, 13 + {"v1.13.0", false}, 14 + {"v1.13.0-alpha", false}, 15 + {"v1.0.0", false}, 16 + {"v2.0.0", true}, 17 + {"1.14.0", true}, 18 + {"1.13.99", false}, 19 + {"(devel)", true}, 20 + {"", false}, 21 + {"garbagio-furioso", false}, 22 + {"v1", false}, 23 + {"vX.Y.Z", false}, 24 + {"unknown", false}, 25 + {"unknown-abc1234", false}, 26 + {"unknown-abc1234-modified", false}, 27 + } 28 + for _, c := range cases { 29 + t.Run(c.in, func(t *testing.T) { 30 + if got := atLeast114(c.in); got != c.want { 31 + t.Errorf("atLeast114(%q) = %v, want %v", c.in, got, c.want) 32 + } 33 + }) 34 + } 35 + }
+4 -1
appview/ingester_repo.go
··· 371 371 if err != nil { 372 372 return false, fmt.Errorf("verify repo ownership: %w", err) 373 373 } 374 - if result.OwnerDid.String() != eventDid { 374 + if result.OwnerDid == "" { 375 + l.Warn("knot lacks RepoDescribeRepo, skipping owner check; upgrade knot to 1.14+", 376 + "repoDid", repoDid, "knot", result.KnotURL.String()) 377 + } else if result.OwnerDid.String() != eventDid { 375 378 l.Warn("rejecting repo event: owner mismatch", 376 379 "repoDid", repoDid, 377 380 "claimedOwner", eventDid,
+3 -6
appview/pulls/create.go
··· 11 11 "time" 12 12 13 13 "tangled.org/core/api/tangled" 14 + "tangled.org/core/appview/compat113" 14 15 "tangled.org/core/appview/db" 15 16 "tangled.org/core/appview/models" 16 17 "tangled.org/core/appview/oauth" ··· 301 302 Collection: tangled.RepoPullNSID, 302 303 Repo: userDid.String(), 303 304 Rkey: rkey, 304 - Record: &lexutil.LexiconTypeDecoder{ 305 - Val: &record, 306 - }, 305 + Record: compat113.Pull(&record), 307 306 }) 308 307 if err != nil { 309 308 l.Error("failed to create pull request", "err", err) ··· 403 402 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 404 403 Collection: tangled.RepoPullNSID, 405 404 Rkey: &p.Rkey, 406 - Value: &lexutil.LexiconTypeDecoder{ 407 - Val: &record, 408 - }, 405 + Value: compat113.Pull(&record), 409 406 }, 410 407 }) 411 408 }
+4 -9
appview/pulls/resubmit.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/api/tangled" 10 + "tangled.org/core/appview/compat113" 10 11 "tangled.org/core/appview/db" 11 12 "tangled.org/core/appview/models" 12 13 "tangled.org/core/appview/oauth" ··· 330 331 Repo: userDid.String(), 331 332 Rkey: pull.Rkey, 332 333 SwapRecord: ex.Cid, 333 - Record: &lexutil.LexiconTypeDecoder{ 334 - Val: &record, 335 - }, 334 + Record: compat113.Pull(&record), 336 335 }) 337 336 if err != nil { 338 337 l.Error("failed to update record on PDS", "err", err, "rkey", pull.Rkey) ··· 521 520 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 522 521 Collection: tangled.RepoPullNSID, 523 522 Rkey: &p.Rkey, 524 - Value: &lexutil.LexiconTypeDecoder{ 525 - Val: &record, 526 - }, 523 + Value: compat113.Pull(&record), 527 524 }, 528 525 }) 529 526 } ··· 581 578 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 582 579 Collection: tangled.RepoPullNSID, 583 580 Rkey: op.Rkey, 584 - Value: &lexutil.LexiconTypeDecoder{ 585 - Val: &record, 586 - }, 581 + Value: compat113.Pull(&record), 587 582 }, 588 583 }) 589 584 }
+7 -3
appview/repo/repo.go
··· 15 15 "tangled.org/core/appview/cloudflare" 16 16 17 17 "tangled.org/core/api/tangled" 18 + "tangled.org/core/appview/compat113" 18 19 "tangled.org/core/appview/config" 19 20 "tangled.org/core/appview/db" 20 21 "tangled.org/core/appview/models" ··· 759 760 Collection: tangled.RepoCollaboratorNSID, 760 761 Repo: currentUser.Did, 761 762 Rkey: rkey, 762 - Record: &lexutil.LexiconTypeDecoder{ 763 - Val: repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt), 764 - }, 763 + Record: compat113.Collaborator(repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt)), 765 764 }) 766 765 // invalid record 767 766 if err != nil { ··· 850 849 return 851 850 } 852 851 852 + if !compat113.KnotSupports114(r.Context(), f.Knot, rp.config.Core.Dev) { 853 + rp.pages.Notice(w, noticeId, "This repository's knot is below v1.14 and does not yet support renames. Ask the knot operator to upgrade.") 854 + return 855 + } 856 + 853 857 newName, err := validateRenameInput(f.Name, f.Rkey, r.FormValue("name")) 854 858 if err != nil { 855 859 rp.pages.Notice(w, noticeId, err.Error())
+4
appview/repoverify/verify.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "errors" 5 6 "fmt" 6 7 "net" 7 8 "net/http" ··· 109 110 client := &indigoxrpc.Client{Host: knot.String(), Client: httpClient} 110 111 out, err := tangled.RepoDescribeRepo(ctx, client, repoDid.String()) 111 112 if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil { 113 + if errors.Is(xrpcErr, xrpcclient.ErrXrpcUnsupported) { 114 + return Result{RepoDid: repoDid, KnotURL: knot}, nil 115 + } 112 116 return Result{}, fmt.Errorf("describeRepo on %s: %w", knot, xrpcErr) 113 117 } 114 118
+10 -5
knotmirror/knotstream/slurper.go
··· 262 262 } 263 263 264 264 type legacyGitRefUpdate struct { 265 - OwnerDid *string `json:"ownerDid,omitempty"` 266 - RepoDid *string `json:"repo,omitempty"` 265 + OwnerDid *string `json:"ownerDid,omitempty"` 266 + Repo *string `json:"repo,omitempty"` 267 + LegacyRepoDid *string `json:"repoDid,omitempty"` 267 268 } 268 269 269 270 type LegacyGitEvent struct { ··· 288 289 // via the stable RepoDid join. Returns (nil, "", nil) when the event has no 289 290 // repoDid (unjoinable) and (nil, key, nil) on a clean miss. 290 291 func (s *KnotSlurper) lookupRepoForRefUpdate(ctx context.Context, evt *LegacyGitEvent) (*models.Repo, string, error) { 291 - if evt.Event.RepoDid == nil || *evt.Event.RepoDid == "" { 292 + raw := evt.Event.Repo 293 + if raw == nil || *raw == "" { 294 + raw = evt.Event.LegacyRepoDid 295 + } 296 + if raw == nil || *raw == "" { 292 297 return nil, "", nil 293 298 } 294 - repoDid := syntax.DID(*evt.Event.RepoDid) 299 + repoDid := syntax.DID(*raw) 295 300 curr, err := db.GetRepoByRepoDid(ctx, s.db, repoDid) 296 301 return curr, repoDid.String(), err 297 302 } ··· 308 313 if curr == nil { 309 314 if lookupKey == "" { 310 315 l.Warn("skipping gitRefUpdate: event has no fields to join on", 311 - "repo_did", evt.Event.RepoDid) 316 + "repo", evt.Event.Repo, "legacy_repo_did", evt.Event.LegacyRepoDid) 312 317 } else { 313 318 // if repo doesn't exist in DB, just ignore the event. That repo is unknown. 314 319 // Hopefully crawler/tap will sync it later.

History

2 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
appview,knotmirror: tolerate 1.13 knots during 1.14 rollout
merge conflicts detected
expand
  • api/tangled/cbor_gen.go:866
  • api/tangled/feedstar.go:5
  • api/tangled/gitrefUpdate.go:29
  • api/tangled/repocollaborator.go:19
  • api/tangled/repoissue.go:22
  • api/tangled/repopull.go:39
  • api/tangled/tangledrepo.go:24
  • cmd/cborgen/cborgen.go:17
  • knotserver/xrpc/merge.go:118
  • lexicons/feed/star.json:10
  • lexicons/git/refUpdate.json:11
  • lexicons/issue/issue.json:9
  • lexicons/pulls/pull.json:65
  • lexicons/repo/collaborator.json:11
  • lexicons/repo/repo.json:6
expand 0 comments
1 commit
expand
appview,knotmirror: tolerate 1.13 knots during 1.14 rollout
expand 0 comments