Files
mesh-delivery/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go
T

153 lines
5.8 KiB
Go

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=<context>`,
// `view`, `note`, `comment.<seq>`, 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, `<owner>/<repo>@<full sha>` 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 <repository>@<number>", 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
}