ForgeGroupEnd gains base; the bridge reads each member's base from the seat's verb groups (Member.base is already in the contract and seatgen's output), keeps groups.<group>.<base>, says one end per base from that base's members, and bridge_group_end_again takes the base. Builds: design 47 G §715, A-236 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
280 lines
13 KiB
Go
280 lines
13 KiB
Go
// Command mesh-delivery-forge-bridge-tools serves the forge bridge's tools (novox/hq ADR 0633 §4, issue 1585; design 51):
|
|
// the runtime launches it for a call. bridge_tags reads a package's git tags through the seat git's verb tags, for the
|
|
// builder's floor. bridge_groups reads the delivery groups the gate holds and what the bridge kept of their ends;
|
|
// bridge_group_end_again writes an ask into the bridge's state end-asks and waits for the bridge's answer there, since the
|
|
// bridge is the other program and serves no request subject (ADR 0437 §4).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
|
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
|
|
|
"mesh-delivery-forge-bridge/internal/bridge"
|
|
"mesh-delivery-forge-bridge/internal/groups"
|
|
)
|
|
|
|
// Tool names.
|
|
const (
|
|
GroupsTool = "bridge_groups"
|
|
EndAgainTool = "bridge_group_end_again"
|
|
// MergedTool answers the merged message of a merge on request (issue 1585).
|
|
MergedTool = "bridge_merged_message"
|
|
)
|
|
|
|
// asker writes an end-asks value and waits for the bridge's answer to it.
|
|
type asker func(key string, value []byte) (groups.EndAsk, error)
|
|
|
|
// Tools are the bridge's tools: groupsRead reads the seat mesh-delivery's groups with what the bridge kept of them.
|
|
func Tools(ask asker, now func() time.Time, groupsRead func() ([]groups.Row, []string, error), merged func(repository, merge string, number int) (json.RawMessage, error), forge bridge.Caller) []stdio.Tool {
|
|
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) }
|
|
return []stdio.Tool{
|
|
{Name: bridge.TagsTool,
|
|
Description: "A package's git tags on the forge, for the builder's floor (design 51): every tag of repository whose name starts " +
|
|
"with prefix (go/v for a Go package in the go directory, v for one at the root), sorted. Read-only; read through the seat " +
|
|
"git's verb tags, at most " + fmt.Sprint(bridge.TagsMaxPages) + " pages (more says so).",
|
|
Input: map[string]any{"type": "object", "required": []string{"repository"}, "properties": map[string]any{
|
|
"repository": map[string]any{"type": "string", "description": "<owner>/<name>"},
|
|
"prefix": map[string]any{"type": "string", "description": "only tags starting with it"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
return bridge.Tags(context.Background(), forge, str(args, "repository"), str(args, "prefix"))
|
|
}},
|
|
{Name: MergedTool,
|
|
Description: "The merged message of one merge, asked again (issue 1585): the message the bridge wrote on its stream FORGE when the " +
|
|
"pull request was merged into the trunk, built the same way from the forge (the merge commit's trees and the manifests " +
|
|
"and lock files of its module directories), the pull request's number, branch and head from the seat git's verb pull, and " +
|
|
"for a delivery group's member its group and members from the seat mesh-delivery's verb groups. For the indexer, which asks it when it takes in a held merge it does not hold in memory (a restart, a new " +
|
|
"build), and the ask-again a lost message lacked. About 0.5 MB for a repository of many modules. Answers {message} " +
|
|
"with the forge message whole, or why it cannot: the forge gives no merged pull request with that commit, or a seat did not answer. " +
|
|
"A merge made on the forge (a recovery merge, the operator's own) is read the same way.",
|
|
Input: map[string]any{"type": "object", "required": []string{"repository", "merge"}, "properties": map[string]any{
|
|
"repository": map[string]any{"type": "string", "description": "the repository, owner/name, as the forge names it"},
|
|
"merge": map[string]any{"type": "string", "description": "the merge commit, 40 hex digits"},
|
|
"number": map[string]any{"type": "integer", "description": "the pull request's number, when known: the bridge reads that pull request and checks its merge commit"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
number, _ := args["number"].(float64)
|
|
raw, err := merged(str(args, "repository"), str(args, "merge"), int(number))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"message": raw}, nil
|
|
}},
|
|
{Name: GroupsTool,
|
|
Description: "The delivery groups the seat mesh-delivery's verb groups answers (more than one member), each with its members' delivery " +
|
|
"state and whether each merged message is written on the forge stream (with the merge commit it named), and whether the " +
|
|
"group's end (forge.group.merged) is written: ended, or open with why (gathered, or every message written and its end " +
|
|
"not written yet). An open group with every member's message written is " +
|
|
"what bridge_group_end_again says the end of. Each group names its last end ask (who, why, when) and the bridge's " +
|
|
"answer to it, from the state end-asks. A group's name narrows it to that group; open lists only open ones.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"group": map[string]any{"type": "string", "description": "only this group, by its id (its branch)"},
|
|
"open": map[string]any{"type": "boolean", "description": "only the groups whose end is not written"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
rows, unread, err := groupsRead()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
only, _ := args["open"].(bool)
|
|
group := str(args, "group")
|
|
kept := rows[:0]
|
|
for _, r := range rows {
|
|
if (group != "" && r.Group != group) || (only && !r.Open) {
|
|
continue
|
|
}
|
|
kept = append(kept, r)
|
|
}
|
|
return map[string]any{"groups": kept, "unread": unread}, nil
|
|
}},
|
|
{Name: EndAgainTool,
|
|
Description: "Say a delivery group's end (forge.group.merged) again, by hand, for a group whose end the indexer did not take " +
|
|
"in (a group bridge_groups names as merged with its end not written, or one the indexer waits for). The bridge " +
|
|
"writes it again from the seat mesh-delivery's groups, with the time appended to the message id so the stream does not drop it as " +
|
|
"a duplicate; the indexer takes an end for a group it already took in without changing anything. Refused while " +
|
|
"a member's merged message is not written. Answers whether it was written.",
|
|
Input: map[string]any{"type": "object", "required": []string{"group", "base"}, "properties": map[string]any{
|
|
"group": map[string]any{"type": "string", "description": "the group, by its id (its branch)"},
|
|
"base": map[string]any{"type": "string", "description": "the watched branch its members merged into, whose end is said again"},
|
|
"why": map[string]any{"type": "string", "description": "why its end is said again, in plain words"}}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
key, value, err := groups.NewEndAsk(str(args, "group"), str(args, "base"), str(args, "why"), stdio.CallerOf(args), now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
a, err := ask(key, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"group": a.Group, "base": a.Base, "written": a.Written, "answer": a.Answer, "by": a.By, "why": a.Why}, nil
|
|
}},
|
|
}
|
|
}
|
|
|
|
// MergedOf is the merged message of the merge of repository at commit merge, as the bridge writes it: the pull request's
|
|
// number, branch and head through the seat git's verb pull (by number when given, else by the merge commit; a merge made
|
|
// on the forge is read the same way), the rest from the forge through trees and files (bridge.MergedMessage), and, for a
|
|
// member of a delivery group the seat mesh-delivery names (of), the group and its members as the process sets them.
|
|
func MergedOf(of []seat.Group, repository, merge string, number int, trees bridge.Trees, files bridge.Files, forge bridge.Caller) (json.RawMessage, error) {
|
|
owner, repo, ok := strings.Cut(repository, "/")
|
|
if !ok || owner == "" || repo == "" {
|
|
return nil, fmt.Errorf("%q is not owner/name", repository)
|
|
}
|
|
fp, err := bridge.ForgeMade(context.Background(), forge, owner, repo, number, merge)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the forge gives no merged pull request of %s with the commit %s: %w", repository, merge, err)
|
|
}
|
|
msg, err := bridge.MergedMessage(owner, repo, fp.Number, fp.Branch, fp.Head, merge, fp.Base, trees, files)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the merged message of %s at %s cannot be built: %w", repository, merge, err)
|
|
}
|
|
f, err := forgemsg.DecodeForge(msg.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if g, ok := groups.Named(of, f.Branch, f.Repository, f.Number); ok {
|
|
f.Group, f.Members = g.ID, groups.MembersOf(g)
|
|
}
|
|
return forgemsg.EncodeForge(f)
|
|
}
|
|
|
|
// read is the groups the seat mesh-delivery answers with what the bridge kept of them: the merged messages written and
|
|
// their ends in performed, and the last ask of each in end-asks with the bridge's answer.
|
|
func read(of []seat.Group, performed, asks stdio.KeptState) ([]groups.Row, []string, error) {
|
|
pkeys, err := performed.Keys()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("the bridge's performed state cannot be listed: %w", err)
|
|
}
|
|
published, ended := map[string]string{}, map[string]string{}
|
|
for _, k := range pkeys {
|
|
into := ended
|
|
switch {
|
|
case strings.HasPrefix(k, "published."):
|
|
into = published
|
|
case !strings.HasPrefix(k, "groups."):
|
|
continue
|
|
}
|
|
if e, err := performed.Get(k); err == nil && e != nil {
|
|
into[k] = strings.TrimSpace(string(e.Value))
|
|
}
|
|
}
|
|
akeys, err := asks.Keys()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("the bridge's state %s cannot be listed: %w", groups.EndAsksState, err)
|
|
}
|
|
askRaws := map[string][]byte{}
|
|
for _, k := range akeys {
|
|
if e, err := asks.Get(k); err == nil && e != nil {
|
|
askRaws[k] = e.Value
|
|
}
|
|
}
|
|
rows, unread := groups.Rows(of, published, ended, askRaws)
|
|
return rows, unread, nil
|
|
}
|
|
|
|
// askEnd writes the end-asks value at key and waits at most bound for the bridge's answer.
|
|
func askEnd(s stdio.KeptState, bound time.Duration) asker {
|
|
wait := askState(s, groups.EndAsksBucket, bound, func(v []byte) bool { a, err := groups.DecodeEndAsk(v); return err == nil && a.Done() })
|
|
return func(key string, value []byte) (groups.EndAsk, error) {
|
|
v, err := wait(key, value)
|
|
if err != nil {
|
|
return groups.EndAsk{}, err
|
|
}
|
|
return groups.DecodeEndAsk(v)
|
|
}
|
|
}
|
|
|
|
// askState writes an ask's value at key of the state s (its bucket named bucket) and waits, on a watch of the key, at most
|
|
// bound for the bridge's answer, a later value that done says is answered.
|
|
func askState(s stdio.KeptState, bucket string, bound time.Duration, done func([]byte) bool) func(key string, value []byte) ([]byte, error) {
|
|
return func(key string, value []byte) ([]byte, error) {
|
|
answered := make(chan []byte, 1)
|
|
var rev uint64
|
|
var mu sync.Mutex
|
|
take := func(revision uint64, v []byte) {
|
|
mu.Lock()
|
|
mine := rev
|
|
mu.Unlock()
|
|
if mine == 0 || revision <= mine || !done(v) {
|
|
return
|
|
}
|
|
select {
|
|
case answered <- v:
|
|
default:
|
|
}
|
|
}
|
|
if err := s.Watch(key, func(c stdio.StateChange) error {
|
|
if c.Op == "put" {
|
|
take(c.Revision, c.Value)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("watching %s.%s: %w", bucket, key, err)
|
|
}
|
|
r, err := s.Put(key, json.RawMessage(value))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the ask could not be written to %s: %w", bucket, err)
|
|
}
|
|
mu.Lock()
|
|
rev = r
|
|
mu.Unlock()
|
|
if e, err := s.Get(key); err == nil && e != nil {
|
|
take(e.Revision, e.Value)
|
|
}
|
|
wait, cancel := context.WithTimeout(context.Background(), bound) // a context's bound, no timer (ADR 0437 §8)
|
|
defer cancel()
|
|
select {
|
|
case v := <-answered:
|
|
return v, nil
|
|
case <-wait.Done():
|
|
return nil, fmt.Errorf("the bridge did not answer within %s: its answer will be in %s.%s", bound, bucket, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// askCaller calls a tool as this bundle's module, through the runtime (stdio.Ask), the module's own invokes granting it.
|
|
func askCaller(_ context.Context, key string, body any) (json.RawMessage, error) {
|
|
return stdio.Ask(key, body)
|
|
}
|
|
|
|
func main() {
|
|
performed, asks := stdio.State(groups.PerformedState), stdio.State(groups.EndAsksState)
|
|
tools := Tools(askEnd(asks, 25*time.Second), time.Now,
|
|
func() ([]groups.Row, []string, error) {
|
|
of, err := groups.Read(context.Background(), askCaller)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return read(of, performed, asks)
|
|
},
|
|
func(repository, merge string, number int) (json.RawMessage, error) {
|
|
ctx := context.Background()
|
|
of, err := groups.Read(ctx, askCaller)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return MergedOf(of, repository, merge, number, bridge.TreesVia(ctx, askCaller), bridge.FilesVia(ctx, askCaller), askCaller)
|
|
}, askCaller)
|
|
owedAsks := stdio.State(groups.OwedAsksState)
|
|
recorder := meshhandactsclient.Recorder{Ask: stdio.Ask}
|
|
tools = append(tools, OwedTools(askState(owedAsks, groups.OwedAsksBucket, 25*time.Second, func(v []byte) bool {
|
|
a, err := groups.DecodeOwedAsk(v)
|
|
return err == nil && a.Done()
|
|
}), func() ([]OwedRow, error) { return performedRows(performed) }, func(what, why string) error {
|
|
_, err := recorder.Record(meshhandactsclient.Request{What: what, Why: why, Cause: "owed-dropped"})
|
|
return err
|
|
}, time.Now)...)
|
|
if err := stdio.Serve("", tools); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[mesh-delivery-forge-bridge-tools] %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|