package bridge import ( "fmt" "slices" "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.`, or `merge`), 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), merge.ts gitea_merge_pull_request (owner, repo, number, head, approved_heads). const ( commitStatusTool = "gitea.gitea_commit_status" deliveryViewTool = "gitea.gitea_delivery_view" noteAppendTool = "gitea.gitea_note_append" addCommentTool = "gitea.gitea_add_comment" mergePullTool = "gitea.gitea_merge_pull_request" ) // MergeAct is the name of the act of merging a pull request. const MergeAct = "merge" 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 } // MergeActs turns the value of a merges key (mesh-sdk go/delivery EncodeMerge) into the merges to perform: one for each // pull request in the state merging, in the order the value names them, at the head it expects and with the approved // heads of every member of the group, `/@` sorted (the merge tool's approved_heads). A pull // request merged or refused already gets none. A value with no approved heads is an error: the forge's merge tool // needs them for a group. func MergeActs(value []byte) ([]Act, error) { m, err := delivery.DecodeMerge(value) if err != nil { return nil, err } var handed []delivery.MergePull for _, p := range m.Pulls { if p.State == delivery.MergeMerging { handed = append(handed, p) } } if len(handed) == 0 { return nil, nil } if len(m.Approved) == 0 { return nil, fmt.Errorf("the merge of the group %s names no approved heads, so the forge's merge tool has nothing to merge at", m.Group) } approved := make([]string, 0, len(m.Approved)) for key, sha := range m.Approved { repository, _, ok := strings.Cut(key, "@") if !ok { return nil, fmt.Errorf("the approved head %q of the group %s is not @", key, m.Group) } approved = append(approved, repository+"@"+sha) } slices.Sort(approved) var acts []Act for _, p := range handed { owner, repo := ownerRepo(p.Repository) acts = append(acts, Act{Name: MergeAct, Repository: p.Repository, Number: p.Number, Head: p.Head.Head, Call: Call{Tool: mergePullTool, Args: map[string]any{"owner": owner, "repo": repo, "number": p.Number, "head": p.Head.Head, "approved_heads": approved}}}) } return acts, 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 }