diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index fb59585..691a687 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019195003-4580d80a32d9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index f74bae2..d96e678 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019195003-4580d80a32d9 h1:XL1dbtSleGAixg6FX4gEnSG/AJxRG/tWw6/7U1RuzHc= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019195003-4580d80a32d9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69 h1:kvtV/SPsdRVUL/BZ3xxGS6U8YrXy/t7TRBjVrD6VLw8= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go b/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go new file mode 100644 index 0000000..c15919b --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go @@ -0,0 +1,106 @@ +package bridge + +import ( + "fmt" + "strings" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// Call is a call of a forge tool: the tool by its manifest key and its arguments. +type Call struct { + Tool string + Args map[string]any +} + +// Act is one thing the bridge does on the forge for a key of the gate: the act's name (an owed key's act, `status=`, +// `view`, `note`, `comment.`), the pull request and head it is on (Number 0 for a commit no pull +// request names) and the forge call that performs it. +type Act struct { + Name string + Repository string + Number int + Head string + Call Call +} + +// The forge's tools, by manifest key; their arguments are the ones of mesh-catalog modules/gitea: delivery.ts +// commitStatusTool (owner, repo, sha, context, state, description, target_url), tools/index.ts gitea_delivery_view +// (owner, repo, number, body), gitea_note_append (owner, repo, sha, ref, line), gitea_add_comment (owner, repo, +// number, body). +const ( + commitStatusTool = "gitea.gitea_commit_status" + deliveryViewTool = "gitea.gitea_delivery_view" + noteAppendTool = "gitea.gitea_note_append" + addCommentTool = "gitea.gitea_add_comment" +) + +func ownerRepo(repository string) (owner, repo string) { + owner, repo, _ = strings.Cut(repository, "/") + return owner, repo +} + +// OwedAct turns an owed key and its value (mesh-sdk go/delivery OwedKey, EncodeOwed) into the forge call that performs +// it. A key that is no owed key, a value that is no owed act, a value whose act is not the key's, and a view or a comment +// on no pull request are errors, each saying why. +func OwedAct(key string, value []byte) (Act, error) { + repository, number, head, name, err := delivery.ParseOwedKey(key) + if err != nil { + return Act{}, err + } + o, err := delivery.DecodeOwed(value) + if err != nil { + return Act{}, fmt.Errorf("the owed act %s: %w", key, err) + } + kind, context, err := delivery.ParseOwedAct(name) + if err != nil { + return Act{}, err + } + if o.Act != kind { + return Act{}, fmt.Errorf("the owed act %s says %q, not the %q its key names", key, o.Act, kind) + } + owner, repo := ownerRepo(repository) + a := Act{Name: name, Repository: repository, Number: number, Head: head} + needsPull := func() error { + if number <= 0 { + return fmt.Errorf("the owed %s %s is on a commit no pull request names, and it needs the pull request", kind, key) + } + return nil + } + switch kind { + case delivery.OwedStatus: + if o.Context != context { + return Act{}, fmt.Errorf("the owed status %s says the context %q, not the %q its key names", key, o.Context, context) + } + a.Call = Call{Tool: commitStatusTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "context": o.Context, + "state": o.State, "description": o.Text, "target_url": o.Target}} + case delivery.OwedView: + if err := needsPull(); err != nil { + return Act{}, err + } + a.Call = Call{Tool: deliveryViewTool, Args: map[string]any{"owner": owner, "repo": repo, "number": number, "body": o.Text}} + case delivery.OwedNote: + a.Call = Call{Tool: noteAppendTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "ref": o.Ref, "line": o.Text}} + case delivery.OwedComment: + if err := needsPull(); err != nil { + return Act{}, err + } + a.Call = Call{Tool: addCommentTool, Args: map[string]any{"owner": owner, "repo": repo, "number": number, "body": o.Text}} + } + return a, nil +} + +// RefusedMessage is the refused message the bridge writes on FORGE when the forge refuses an act: the act, the pull +// request and head it was on, and the forge's own words. +func RefusedMessage(a Act, words string) (Message, error) { + event, err := delivery.ForgeEvent(a.Repository, delivery.ForgeRefused) + if err != nil { + return Message{}, err + } + body, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeRefused, Repository: a.Repository, Number: a.Number, + Head: a.Head, Act: a.Name, Words: words}) + if err != nil { + return Message{}, err + } + return Message{Event: event, Data: body}, nil +} diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go new file mode 100644 index 0000000..a4d414b --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go @@ -0,0 +1,124 @@ +package bridge + +import ( + "reflect" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +const ( + headA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +) + +// owedValue is an owed value as the SDK writes it (mesh-sdk go/delivery EncodeOwed), and its key as OwedKey spells it. +func owedValue(t *testing.T, repository string, number int, act string, o delivery.Owed) (string, []byte) { + t.Helper() + key, err := delivery.OwedKey(repository, number, headA, act) + if err != nil { + t.Fatal(err) + } + b, err := delivery.EncodeOwed(o) + if err != nil { + t.Fatal(err) + } + return key, b +} + +func TestOwedStatusIsACommitStatusCall(t *testing.T) { // 1341/AC1 + key, v := owedValue(t, "novox/mesh-delivery", 12, "status=mesh/merge-gate", + delivery.Owed{Act: delivery.OwedStatus, Context: "mesh/merge-gate", State: "pending", Text: "waiting", Target: "https://forge.example/novox/mesh-delivery/pulls/12"}) + a, err := OwedAct(key, v) + if err != nil { + t.Fatal(err) + } + want := Call{Tool: "gitea.gitea_commit_status", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "sha": headA, + "context": "mesh/merge-gate", "state": "pending", "description": "waiting", "target_url": "https://forge.example/novox/mesh-delivery/pulls/12"}} + if !reflect.DeepEqual(a.Call, want) { + t.Errorf("call = %+v, want %+v", a.Call, want) + } + if a.Repository != "novox/mesh-delivery" || a.Number != 12 || a.Head != headA || a.Name != "status=mesh/merge-gate" { + t.Errorf("act = %+v", a) + } +} + +func TestOwedViewNoteAndCommentCalls(t *testing.T) { // 1341/AC2 + cases := []struct { + act string + o delivery.Owed + want Call + }{ + {"view", delivery.Owed{Act: delivery.OwedView, Text: "the view"}, + Call{Tool: "gitea.gitea_delivery_view", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "the view"}}}, + {"note", delivery.Owed{Act: delivery.OwedNote, Text: "a line", Ref: "mesh-plan"}, + Call{Tool: "gitea.gitea_note_append", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "sha": headA, "ref": "mesh-plan", "line": "a line"}}}, + {"comment.2", delivery.Owed{Act: delivery.OwedComment, Text: "hello"}, + Call{Tool: "gitea.gitea_add_comment", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "hello"}}}, + } + for _, c := range cases { + key, v := owedValue(t, "novox/mesh-delivery", 12, c.act, c.o) + a, err := OwedAct(key, v) + if err != nil { + t.Fatalf("%s: %v", c.act, err) + } + if !reflect.DeepEqual(a.Call, c.want) { + t.Errorf("%s: call = %+v, want %+v", c.act, a.Call, c.want) + } + } +} + +func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) { + key, v := owedValue(t, "novox/mesh-delivery", 0, "note", delivery.Owed{Act: delivery.OwedNote, Text: "l", Ref: "mesh-plan"}) + if _, err := OwedAct(key, v); err != nil { + t.Errorf("a note on a commit no pull request names: %v", err) + } + key, v = owedValue(t, "novox/mesh-delivery", 0, "view", delivery.Owed{Act: delivery.OwedView, Text: "v"}) + if _, err := OwedAct(key, v); err == nil { + t.Error("a view of no pull request was taken") + } +} + +func TestOwedActOfADottedRepositoryIsUnescaped(t *testing.T) { + key, v := owedValue(t, "novox/my.repo", 3, "comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "x"}) + a, err := OwedAct(key, v) + if err != nil { + t.Fatal(err) + } + if a.Call.Args["repo"] != "my.repo" || a.Repository != "novox/my.repo" { + t.Errorf("act = %+v", a) + } +} + +func TestOwedActThatDisagreesWithItsKeyIsRefused(t *testing.T) { + key, _ := owedValue(t, "novox/mesh-delivery", 12, "view", delivery.Owed{Act: delivery.OwedView, Text: "v"}) + v, _ := delivery.EncodeOwed(delivery.Owed{Act: delivery.OwedComment, Text: "v"}) + if _, err := OwedAct(key, v); err == nil { + t.Error("a comment under a view's key was taken") + } + if _, err := OwedAct("nonsense", v); err == nil { + t.Error("a key that is no owed key was taken") + } + if _, err := OwedAct(key, []byte(`{"v":1`)); err == nil { + t.Error("a value that is not json was taken") + } +} + +func TestRefusedMessageNamesTheActAndTheForgesWords(t *testing.T) { // 1341/AC3 + a := Act{Name: "comment.1", Repository: "novox/mesh-delivery", Number: 12, Head: headA} + m, err := RefusedMessage(a, "novox/mesh-delivery#12 is not merged: its head moved") + if err != nil { + t.Fatal(err) + } + if m.Event != "forge.novox.mesh-delivery.refused" { + t.Errorf("event = %q", m.Event) + } + f, err := delivery.DecodeForge(m.Data) + if err != nil { + t.Fatal(err) + } + want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeRefused, Repository: "novox/mesh-delivery", Number: 12, Head: headA, + Act: "comment.1", Words: "novox/mesh-delivery#12 is not merged: its head moved"} + if !delivery.SameForge(f, want) { + t.Errorf("message = %+v", f) + } +}