Add the forge kind open: every open pull request into trunk (issue 1279, ADR 0443) #42

Merged
mesh-tools merged 1 commits from fix/1279-consumers-from-now into main 2026-10-19 20:22:46 +00:00
2 changed files with 84 additions and 6 deletions
Showing only changes of commit 4580d80a32 - Show all commits
+48 -6
View File
@@ -41,8 +41,19 @@ const (
ForgePull = "pull"
ForgeMerged = "merged"
ForgeRefused = "refused"
ForgeOpen = "open"
)
// OpenPull names an open pull request into the trunk: the repository as `<owner>/<name>` and the number.
type OpenPull struct {
Repository string `json:"repository"`
Number int `json:"number"`
}
// OpenEvent is the name of an open message: `forge.open`. It names no repository: it says every open pull request of
// every repository the forge bridge reads (novox/hq ADR 0443 §5).
func OpenEvent() string { return "forge." + ForgeOpen }
// The actions of a pull message.
const (
PullOpened = "opened"
@@ -106,18 +117,21 @@ func checkRepository(repository string) error {
func checkForgeKind(kind string) error {
switch kind {
case ForgePull, ForgeMerged, ForgeRefused:
case ForgePull, ForgeMerged, ForgeRefused, ForgeOpen:
return nil
}
return fmt.Errorf("delivery: the forge kind %q is not pull, merged or refused", kind)
return fmt.Errorf("delivery: the forge kind %q is not pull, merged, refused or open", kind)
}
// ForgeEvent is the name of a forge message of the given kind: `forge.<owner>.<name>.<kind>`.
func ForgeEvent(repository, kind string) (string, error) {
if err := checkRepository(repository); err != nil {
if err := checkForgeKind(kind); err != nil {
return "", err
}
if err := checkForgeKind(kind); err != nil {
if kind == ForgeOpen {
return "", fmt.Errorf("delivery: an open message names no repository, its event is %s", OpenEvent())
}
if err := checkRepository(repository); err != nil {
return "", err
}
owner, name, _ := strings.Cut(repository, "/")
@@ -126,6 +140,9 @@ func ForgeEvent(repository, kind string) (string, error) {
// ParseForgeEvent is the inverse of ForgeEvent.
func ParseForgeEvent(event string) (repository, kind string, err error) {
if event == OpenEvent() {
return "", ForgeOpen, nil
}
parts := strings.Split(event, ".")
if len(parts) != 4 || parts[0] != "forge" {
return "", "", fmt.Errorf("delivery: %q is not a forge event, forge.<owner>.<name>.<kind>", event)
@@ -259,13 +276,20 @@ type Forge struct {
Root string `json:"root,omitempty"`
Act string `json:"act,omitempty"`
Words string `json:"words,omitempty"`
Open []OpenPull `json:"open,omitempty"`
}
func (f Forge) check() error {
if err := checkRepository(f.Repository); err != nil {
if err := checkForgeKind(f.Kind); err != nil {
return err
}
if err := checkForgeKind(f.Kind); err != nil {
if f.Kind == ForgeOpen {
return f.checkOpen()
}
if len(f.Open) > 0 {
return fmt.Errorf("delivery: only an open message names open pull requests")
}
if err := checkRepository(f.Repository); err != nil {
return err
}
switch f.Kind {
@@ -322,6 +346,24 @@ func (f Forge) check() error {
return nil
}
// checkOpen refuses an open message that names a repository, a branch, a head or anything of the other kinds, or
// an open pull request that is not `<owner>/<name>` and a number.
func (f Forge) checkOpen() error {
if f.Repository != "" || f.Branch != "" || f.Number != 0 || f.Head != "" || f.Action != "" || len(f.Dirs) > 0 || f.DirsKnown ||
f.Merge != "" || len(f.Trees) > 0 || f.Root != "" || f.Act != "" || f.Words != "" {
return fmt.Errorf("delivery: an open message carries only the open pull requests it names")
}
for _, p := range f.Open {
if err := checkRepository(p.Repository); err != nil {
return err
}
if p.Number <= 0 {
return fmt.Errorf("delivery: the open pull request of %s has no number", p.Repository)
}
}
return nil
}
// EncodeSource is s as JSON at Version; it refuses a source with no repository or an empty head.
func EncodeSource(s Source) ([]byte, error) {
if err := s.check(); err != nil {
+36
View File
@@ -65,6 +65,41 @@ func TestSourceKeys(t *testing.T) { // 1200/AC1
}
}
func TestForgeOpenKind(t *testing.T) { // 1279/AC5
ev := OpenEvent()
if ev != "forge.open" {
t.Fatalf("open event = %q", ev)
}
repo, kind, err := ParseForgeEvent(ev)
if err != nil || repo != "" || kind != ForgeOpen {
t.Fatalf("parse open event = %q %q %v", repo, kind, err)
}
// An empty list is a message too: nothing is open.
data, err := EncodeForge(Forge{Kind: ForgeOpen, Open: []OpenPull{}})
if err != nil {
t.Fatalf("empty open: %v", err)
}
if f, err := DecodeForge(data); err != nil || f.Kind != ForgeOpen || len(f.Open) != 0 {
t.Errorf("empty open round trip: %+v %v", f, err)
}
for name, f := range map[string]Forge{
"a repository": {Kind: ForgeOpen, Repository: "a/b", Open: []OpenPull{{Repository: "a/b", Number: 1}}},
"a bad entry": {Kind: ForgeOpen, Open: []OpenPull{{Repository: "a", Number: 1}}},
"a zero number": {Kind: ForgeOpen, Open: []OpenPull{{Repository: "a/b"}}},
"a head": {Kind: ForgeOpen, Head: "x", Open: []OpenPull{{Repository: "a/b", Number: 1}}},
} {
if _, err := EncodeForge(f); err == nil {
t.Errorf("open with %s accepted", name)
}
}
if _, err := EncodeForge(Forge{Kind: ForgePull, Repository: "a/b", Number: 1, Head: "h", Action: "updated", Open: []OpenPull{{Repository: "a/b", Number: 1}}}); err == nil {
t.Error("a pull message naming open pull requests accepted")
}
if _, err := ForgeEvent("a/b", ForgeOpen); err == nil {
t.Error("an open event for a repository accepted")
}
}
func TestHeadsHash(t *testing.T) { // 1200/AC2
a := Head{Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"}
b := Head{Repository: "novox/mesh-control", Number: 7, Head: "bbbb"}
@@ -113,6 +148,7 @@ func sampleForges() []Forge {
{Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa", Merge: "cccc",
Trees: map[string]string{"modules/a": "t1"}, Root: "t0"},
{Kind: ForgeRefused, Repository: "novox/mesh-sdk", Act: "novox/mesh-sdk.36.aaaa.view", Words: "403 forbidden"},
{Kind: ForgeOpen, Open: []OpenPull{{Repository: "novox/mesh-sdk", Number: 36}, {Repository: "novox/mesh-control", Number: 7}}},
}
}