diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go new file mode 100644 index 00000000..348213dd --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge-tools/main.go @@ -0,0 +1,42 @@ +// Command mesh-delivery-forge-bridge-tools serves the forge bridge's tools (novox/hq design 51): bridge_tags, a +// package's git tags read from the forge through the forge module's gitea_api, for the builder's floor. The runtime +// launches it for a call; the bridge's program serves no request subject (ADR 0437 §8). +package main + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + stdio "git.novox.be/novox/mesh-sdk/go" + + "mesh-delivery-forge-bridge/internal/bridge" +) + +// tools are the bridge's tools over api. +func tools(api bridge.API) []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 from the forge " + + "through the forge module's gitea_api, at most " + fmt.Sprint(bridge.TagsMaxPages*bridge.TagsPageSize) + " tags (more says so).", + Input: map[string]any{"type": "object", "required": []string{"repository"}, "properties": map[string]any{ + "repository": map[string]any{"type": "string", "description": "/"}, + "prefix": map[string]any{"type": "string", "description": "only tags starting with it"}}}, + Run: func(args map[string]any) (any, error) { + return bridge.Tags(api, str(args, "repository"), str(args, "prefix")) + }, + }} +} + +func main() { + api := func(path string) (json.RawMessage, error) { + return stdio.Ask("gitea.gitea_api", map[string]any{"path": path, "method": "GET"}) + } + if err := stdio.Serve("", tools(api)); err != nil { + fmt.Fprintf(os.Stderr, "[mesh-delivery-forge-bridge-tools] %v\n", err) + os.Exit(1) + } +} diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go index 84644d6f..3556adf7 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go @@ -118,6 +118,7 @@ accounts { subscribe { allow: [ "_INBOX.n1.mesh-delivery-forge-bridge.>" "mesh.assignment.n1.mesh-delivery-forge-bridge" + "mesh.mod.gitea.event.forge.delete" "mesh.mod.gitea.event.pull.closed" "mesh.mod.gitea.event.pull.merged" "mesh.mod.gitea.event.pull.updated" @@ -235,7 +236,7 @@ func startBed(t *testing.T, openReply string, prepare func(ctx context.Context, } if _, err := js.CreateOrUpdateConsumer(ctx, "EVENTS", jetstream.ConsumerConfig{ Durable: "n1_mesh-delivery-forge-bridge", AckPolicy: jetstream.AckExplicitPolicy, DeliverPolicy: jetstream.DeliverNewPolicy, - FilterSubjects: []string{"mesh.mod.gitea.event.pull.updated", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.closed"}, + FilterSubjects: []string{"mesh.mod.gitea.event.pull.updated", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.closed", "mesh.mod.gitea.event.forge.delete"}, }); err != nil { t.Fatal(err) } @@ -293,7 +294,8 @@ func startBed(t *testing.T, openReply string, prepare func(ctx context.Context, // TestBridgeWritesPullMessages starts the program the way the node does (its entry, run, with MESH_BROKER_FILE). func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2 - _, js, ctx, _ := startBridge(t, `{"result":{"pulls":[]}}`) + ctl, js, ctx, _ := startBridge(t, `{"result":{"pulls":[]}}`) + defer branchDeletedCase(t, ctl.ConnectedUrl(), js, ctx) // gitea.pull.updated as the forge module publishes it: the body of trunk.ts:233-250 on mesh.mod.gitea.event.pull.updated. ev, _ := json.Marshal(map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "title": "t", "body": "b", "base": "main", @@ -346,6 +348,42 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2 t.Errorf("the event is not acknowledged: %+v", ci) } +// branchDeletedCase is DLV-FB1's case of the bridge's whole-module test (design 51): the forge module's forge.delete of +// a branch is a branch-deleted message on FORGE, and a forge.delete from another account than the forge module's is +// refused by the bus. +func branchDeletedCase(t *testing.T, srv string, js jetstream.JetStream, ctx context.Context) { + t.Helper() + ev, _ := json.Marshal(map[string]any{"owner": "novox", "repo": "mesh-sdk", "ref": "feat/x", "ref_type": "branch", "sha": head}) + if _, err := js.Publish(ctx, "mesh.mod.gitea.event.forge.delete", ev); err != nil { + t.Fatal(err) + } + forge, err := js.Stream(ctx, "FORGE") + if err != nil { + t.Fatal(err) + } + subject := "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-sdk.branch-deleted" + var raw *jetstream.RawStreamMsg + for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { + if raw, err = forge.GetLastMsgForSubject(ctx, subject); err == nil { + break + } + } + if err != nil || !strings.Contains(string(raw.Data), `"branch":"feat/x"`) { + t.Fatalf("no branch-deleted message on FORGE within 20 s: %v", err) + } + other, err := nats.Connect(srv, nats.UserInfo("n1.mesh-delivery-forge-bridge", "pw")) + if err != nil { + t.Fatal(err) + } + defer other.Close() + ojs, _ := jetstream.New(other) + octx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + if _, err := ojs.Publish(octx, "mesh.mod.gitea.event.forge.delete", ev); err == nil { + t.Error("a forge.delete from the bridge's account was taken by the bus: only the forge module's account may say it") + } +} + const merge = "89abcdef0123456789abcdef0123456789abcdef" // TestBridgeWritesMergedMessage has a stand-in answer the forge's gitea_commit_trees on its tool subject diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go index 774bc6b1..3d99c591 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go @@ -68,8 +68,62 @@ const ( UpdatedSubject = "mesh.mod.gitea.event.pull.updated" ClosedSubject = "mesh.mod.gitea.event.pull.closed" MergedSubject = "mesh.mod.gitea.event.pull.merged" + // DeletedSubject is the forge module's forge.delete: a branch or a tag deleted (mesh-catalog modules/gitea + // webhook.ts: owner, repo, ref, ref_type, sha). Only the forge module's account may publish it, the subject being + // in its own namespace (design 32 § 2), so no other module can end a branch's tagged versions. + DeletedSubject = "mesh.mod.gitea.event.forge.delete" ) +// ForgeBranchDeleted is the kind of a branch-deleted message on FORGE (novox/hq design 51 *The forge bridge says a +// branch's end*): the builder removes the branch's tagged versions on it, as on a pull request's merged or closed. +// Every existing consumer of FORGE ignores a kind it does not know. +const ForgeBranchDeleted = "branch-deleted" + +// BranchDeleted is the body of a branch-deleted message. +type BranchDeleted struct { + V int `json:"v"` + Kind string `json:"kind"` + Repository string `json:"repository"` + Branch string `json:"branch"` + // Head is the commit the branch named when it was deleted, when the forge said it. + Head string `json:"head,omitempty"` +} + +// deleted is the part of forge.delete the bridge reads. +type deleted struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + Ref string `json:"ref"` + RefType string `json:"ref_type"` + SHA string `json:"sha"` +} + +// branchDeleted turns forge.delete into a branch-deleted message; a deleted tag is not handled. +func branchDeleted(subject string, data []byte) (Message, error) { + var d deleted + if err := json.Unmarshal(data, &d); err != nil { + return Message{}, unreadable(subject, "the body is not json: %v", err) + } + if d.RefType != "branch" { + return Message{}, fmt.Errorf("%w: %s of a %s (%s)", ErrNotHandled, subject, d.RefType, d.Ref) + } + branch := strings.TrimPrefix(d.Ref, "refs/heads/") + switch { + case d.Owner == "" || d.Repo == "" || strings.ContainsAny(d.Owner+d.Repo, "./ *>"): + return Message{}, unreadable(subject, "no owner or repo a forge event can name (%q/%q)", d.Owner, d.Repo) + case branch == "": + return Message{}, unreadable(subject, "no branch") + case branch == Trunk: + return Message{}, fmt.Errorf("%w: %s of the trunk %s", ErrNotHandled, subject, Trunk) + } + body, err := json.Marshal(BranchDeleted{V: delivery.Version, Kind: ForgeBranchDeleted, Repository: d.Owner + "/" + d.Repo, Branch: branch, Head: d.SHA}) + if err != nil { + return Message{}, unreadable(subject, "%v", err) + } + return Message{Event: "forge." + d.Owner + "." + d.Repo + "." + ForgeBranchDeleted, Data: body, + Said: fmt.Sprintf("branch %s of %s/%s deleted: its tagged versions end", branch, d.Owner, d.Repo)}, nil +} + var sha40 = regexp.MustCompile(`^[0-9a-f]{40}$`) // pull is the part of gitea.pull.merged (index.ts:186-207), gitea.pull.updated (mesh-catalog modules/gitea/trunk.ts:233-250) and gitea.pull.closed @@ -115,6 +169,8 @@ func ConvertKept(subject string, data []byte, trees Trees, files Files, kept Kep action = delivery.PullClosed case MergedSubject: action = delivery.PullClosed // unless it merged into the trunk, below + case DeletedSubject: + return branchDeleted(subject, data) default: return Message{}, fmt.Errorf("%w: %s", ErrNotHandled, subject) } diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go index 3bd1116f..9de957a5 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go @@ -63,14 +63,13 @@ func TestUpdatedBecomesAPullMessage(t *testing.T) { // 1235/AC2 (the conversion; if !delivery.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) } -} -func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3 - msg, err := Convert(updatedSubject, updated(map[string]any{"module_dirs": nil, "module_dirs_said": nil}), nil, anyFiles) + // 1235/AC3, merged into this test for the test budget: dirs not said are not known. + msg, err = Convert(updatedSubject, updated(map[string]any{"module_dirs": nil, "module_dirs_said": nil}), nil, anyFiles) if err != nil { t.Fatal(err) } - f, _ := delivery.DecodeForge(msg.Data) + f, _ = delivery.DecodeForge(msg.Data) if f.DirsKnown || len(f.Dirs) != 0 { t.Errorf("dirs = %v known = %v, want not known", f.Dirs, f.DirsKnown) } @@ -171,11 +170,31 @@ func mustDrop(body []byte, key string) []byte { } func TestOtherSubjectsAreNotHandled(t *testing.T) { // 1235 - for _, s := range []string{"mesh.mod.gitea.event.something.else", "mesh.mod.other.event.pull.updated"} { + for _, s := range []string{"mesh.mod.gitea.event.something.else", "mesh.mod.other.event.pull.updated", "mesh.mod.other.event.forge.delete"} { if _, err := Convert(s, updated(nil), nil, anyFiles); !errors.Is(err, ErrNotHandled) { t.Errorf("%s: err = %v, want ErrNotHandled", s, err) } } + // The forge module's forge.delete of a branch is a branch-deleted message (DLV-FB1, design 51); of a tag or the + // trunk it is not handled, and a body naming no repository is unreadable. + del := func(refType, ref string) []byte { + b, _ := json.Marshal(map[string]any{"owner": "novox", "repo": "mesh-sdk", "full_name": "novox/mesh-sdk", "ref": ref, "ref_type": refType, "sha": head}) + return b + } + msg, err := Convert(DeletedSubject, del("branch", "feat/x"), nil, anyFiles) + var bd BranchDeleted + if err != nil || msg.Event != "forge.novox.mesh-sdk.branch-deleted" || json.Unmarshal(msg.Data, &bd) != nil || + bd.Kind != ForgeBranchDeleted || bd.Repository != "novox/mesh-sdk" || bd.Branch != "feat/x" || bd.Head != head || msg.Said == "" { + t.Errorf("forge.delete of a branch = %+v (%s), %v", msg, msg.Data, err) + } + for _, c := range []struct{ refType, ref string }{{"tag", "go/v0.1.13"}, {"branch", "main"}} { + if _, err := Convert(DeletedSubject, del(c.refType, c.ref), nil, anyFiles); !errors.Is(err, ErrNotHandled) { + t.Errorf("forge.delete of %s %s: err = %v, want ErrNotHandled", c.refType, c.ref, err) + } + } + if _, err := Convert(DeletedSubject, []byte(`{"ref":"x","ref_type":"branch"}`), nil, anyFiles); !errors.Is(err, ErrUnreadable) { + t.Errorf("forge.delete naming no repository: err = %v, want ErrUnreadable", err) + } } const merge = "89abcdef0123456789abcdef0123456789abcdef" @@ -395,3 +414,50 @@ func TestTrackSaysWhichHeadOfWhichPullRequestWasWritten(t *testing.T) { // 1279/ t.Error("an open message was tracked") } } + +func TestBridgeTagsAnswersAPackagesTagsByPrefixPageByPage(t *testing.T) { // DLV-FB1, design 51 *The floor* + var asked []string + page := func(n int) json.RawMessage { + var tags []map[string]string + for i := 0; i < n; i++ { + tags = append(tags, map[string]string{"name": "x" + strings.Repeat("y", i)}) + } + b, _ := json.Marshal(tags) + return b + } + api := func(path string) (json.RawMessage, error) { + asked = append(asked, path) + if strings.Contains(path, "page=1&") { + full := page(TagsPageSize - 2) + var tags []map[string]string + _ = json.Unmarshal(full, &tags) + tags = append(tags, map[string]string{"name": "go/v0.1.13"}, map[string]string{"name": "v0.0.1"}) + b, _ := json.Marshal(tags) + return b, nil + } + return json.RawMessage(`[{"name":"go/v0.1.2"}]`), nil + } + a, err := Tags(api, "novox/mesh-sdk", "go/v") + if err != nil || !slicesEqual(a.Tags, []string{"go/v0.1.13", "go/v0.1.2"}) || a.More || len(asked) != 2 || + asked[0] != "/repos/novox/mesh-sdk/tags?page=1&limit=50" { + t.Errorf("Tags = %+v, %v; asked %v", a, err, asked) + } + if _, err := Tags(func(string) (json.RawMessage, error) { return nil, errors.New("down") }, "novox/mesh-sdk", ""); err == nil || !strings.Contains(err.Error(), "page 1") { + t.Errorf("a forge not answering = %v, want an error naming the page", err) + } + if _, err := Tags(api, "mesh-sdk", ""); err == nil { + t.Error("a repository with no owner answered") + } +} + +func slicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/tags.go b/modules/mesh-delivery-forge-bridge/internal/bridge/tags.go new file mode 100644 index 00000000..4ae8c146 --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/tags.go @@ -0,0 +1,68 @@ +package bridge + +import ( + "encoding/json" + "fmt" + "net/url" + "sort" + "strings" +) + +// TagsTool is the forge bridge's tool bridge_tags (novox/hq design 51 *The floor*): a package's git tags, which the +// builder's floor reads as one of its three sources. +const TagsTool = "bridge_tags" + +// TagsPageSize is how many tags one page of the forge's tags answers; TagsMaxPages bounds the read. +const ( + TagsPageSize = 50 + TagsMaxPages = 40 +) + +// API asks the forge one GET of its API (the forge module's gitea_api), answering the body. +type API func(path string) (json.RawMessage, error) + +// TagsAnswer is what bridge_tags answers. +type TagsAnswer struct { + Repository string `json:"repository"` + Prefix string `json:"prefix,omitempty"` + Tags []string `json:"tags"` + // More says the read stopped at TagsMaxPages pages with more left. + More bool `json:"more,omitempty"` +} + +// Tags is bridge_tags: every tag of repository (`/`) whose name starts with prefix (`go/v` for the Go +// package of mesh-sdk's `go` directory, `v` or empty for a package at the root), sorted. A forge that does not answer +// is an error naming the page: the builder then counts this source silent. +func Tags(api API, repository, prefix string) (TagsAnswer, error) { + owner, name, ok := strings.Cut(strings.TrimSpace(repository), "/") + if !ok || owner == "" || name == "" || strings.Contains(name, "/") { + return TagsAnswer{}, fmt.Errorf("repository %q is not /", repository) + } + out := TagsAnswer{Repository: owner + "/" + name, Prefix: prefix, Tags: []string{}} + for page := 1; ; page++ { + if page > TagsMaxPages { + out.More = true + break + } + raw, err := api(fmt.Sprintf("/repos/%s/%s/tags?page=%d&limit=%d", url.PathEscape(owner), url.PathEscape(name), page, TagsPageSize)) + if err != nil { + return TagsAnswer{}, fmt.Errorf("the forge did not answer the tags of %s (page %d): %w", out.Repository, page, err) + } + var tags []struct { + Name string `json:"name"` + } + if err := json.Unmarshal(raw, &tags); err != nil { + return TagsAnswer{}, fmt.Errorf("the forge's tags of %s (page %d) are no list of tags: %w", out.Repository, page, err) + } + for _, t := range tags { + if strings.HasPrefix(t.Name, prefix) { + out.Tags = append(out.Tags, t.Name) + } + } + if len(tags) < TagsPageSize { + break + } + } + sort.Strings(out.Tags) + return out, nil +} diff --git a/modules/mesh-delivery-forge-bridge/manifest_test.go b/modules/mesh-delivery-forge-bridge/manifest_test.go index 19686ac1..3ef9f93d 100644 --- a/modules/mesh-delivery-forge-bridge/manifest_test.go +++ b/modules/mesh-delivery-forge-bridge/manifest_test.go @@ -28,18 +28,20 @@ type state struct { } type artifact struct { - Name string `json:"name"` - Kind string `json:"kind"` - Language string `json:"language"` - System string `json:"system"` - From string `json:"from"` - Binary string `json:"binary"` + Name string `json:"name"` + Kind string `json:"kind"` + Language string `json:"language"` + System string `json:"system"` + From string `json:"from"` + Binary string `json:"binary"` + Loads []string `json:"loads"` } type manifest struct { Module string `json:"module"` Version string `json:"version"` Slug string `json:"slug"` + Tools []string `json:"tools"` Consumes []string `json:"consumes"` Streams []stream `json:"streams"` State []state `json:"state"` @@ -87,13 +89,14 @@ func TestManifestDeclarations(t *testing.T) { // 1235/AC1 t.Errorf("streams = %+v", m.Streams) } // The checker's owed is the forge module's to write (ADR 0521, R4e finding R2-1): the bridge neither hears nor reads it. - if want := []string{"gitea.pull.closed", "gitea.pull.merged", "gitea.pull.updated"}; !reflect.DeepEqual(sorted(m.Consumes), want) { + // gitea.forge.delete: a branch's end, said as a branch-deleted kind (design 51, DLV-FB1). + if want := []string{"gitea.forge.delete", "gitea.pull.closed", "gitea.pull.merged", "gitea.pull.updated"}; !reflect.DeepEqual(sorted(m.Consumes), want) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } if want := []string{"mesh-delivery-gate.merges", "mesh-delivery-gate.owed"}; !reflect.DeepEqual(sorted(m.Reads), want) { t.Errorf("reads = %v, want %v", m.Reads, want) } - if want := []string{"gitea.gitea_add_comment", "gitea.gitea_commit_status", "gitea.gitea_commit_trees", "gitea.gitea_delivery_view", + if want := []string{"gitea.gitea_add_comment", "gitea.gitea_api", "gitea.gitea_commit_status", "gitea.gitea_commit_trees", "gitea.gitea_delivery_view", "gitea.gitea_get_file", "gitea.gitea_merge_pull_request", "gitea.gitea_note_append", "gitea.gitea_open_pulls"}; !reflect.DeepEqual(sorted(m.Invokes), want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } @@ -110,8 +113,12 @@ func TestManifestDeclarations(t *testing.T) { // 1235/AC1 if !reflect.DeepEqual(m.State, []state{{Name: "heads", History: 1}, {Name: "performed", History: 1}, {Name: "inputs", History: 1}}) { // 1279/AC6, 1341, 1126 t.Errorf("state = %+v, want the buckets heads, performed and inputs", m.State) } - // No other state, no bus, no seat, no tool, no event (ADR 0437 §4; ADR 0521: no check.status). - for _, k := range []string{"logs", "emits", "claims", "provides", "requires", "uses", "defines-seats", "tools", "seat", "seats"} { + // One tool, bridge_tags, served by its tools bundle, for the builder's floor (design 51). + if !reflect.DeepEqual(m.Tools, []string{"bridge_tags"}) { + t.Errorf("tools = %v, want bridge_tags", m.Tools) + } + // No other state, no bus, no seat, no event (ADR 0437 §4; ADR 0521: no check.status). + for _, k := range []string{"logs", "emits", "claims", "provides", "requires", "uses", "defines-seats", "seat", "seats"} { if _, ok := top[k]; ok { t.Errorf("the manifest declares %q: the bridge has none", k) } @@ -143,8 +150,13 @@ func TestManifestProcess(t *testing.T) { // 1235/AC1 if _, bus := p["bus"]; bus { t.Errorf("the process declares a bus: it runs on its own broker credential") } - if len(m.Build.Artifacts) != 1 { - t.Fatalf("artifacts = %+v", m.Build.Artifacts) + if len(m.Build.Artifacts) != 2 { + t.Fatalf("artifacts = %+v, want the program and the tools", m.Build.Artifacts) + } + if a := m.Build.Artifacts[1]; a.Name != "tools" || a.Binary != "mesh-delivery-forge-bridge-tools" || !reflect.DeepEqual(a.Loads, []string{a.Binary}) { + t.Errorf("tools artifact = %+v", a) + } else if st, err := os.Stat(a.From); err != nil || !st.IsDir() { + t.Errorf("the tools' source %s: %v", a.From, err) } a := m.Build.Artifacts[0] if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != "mesh-delivery-forge-bridge" || !strings.HasSuffix(a.From, a.Binary) { diff --git a/modules/mesh-delivery-forge-bridge/module.json b/modules/mesh-delivery-forge-bridge/module.json index f152cad0..a3fada48 100644 --- a/modules/mesh-delivery-forge-bridge/module.json +++ b/modules/mesh-delivery-forge-bridge/module.json @@ -2,10 +2,14 @@ "module": "mesh-delivery-forge-bridge", "version": "1", "slug": "forge-bridge", + "tools": [ + "bridge_tags" + ], "consumes": [ "gitea.pull.updated", "gitea.pull.merged", - "gitea.pull.closed" + "gitea.pull.closed", + "gitea.forge.delete" ], "streams": [ { "name": "forge", "events": ["forge.**"] } @@ -27,7 +31,8 @@ "gitea.gitea_add_comment", "gitea.gitea_open_pulls", "gitea.gitea_commit_trees", - "gitea.gitea_get_file" + "gitea.gitea_get_file", + "gitea.gitea_api" ], "own-secrets": { "broker": "${dir:state}/broker" @@ -75,6 +80,17 @@ "system": "arch", "from": "cmd/mesh-delivery-forge-bridge", "binary": "mesh-delivery-forge-bridge" + }, + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-delivery-forge-bridge-tools", + "binary": "mesh-delivery-forge-bridge-tools", + "loads": [ + "mesh-delivery-forge-bridge-tools" + ] } ] }