Merge b2/registry (f2c61815) into b7/registry

This commit is contained in:
2026-11-05 14:57:36 +01:00
40 changed files with 2326 additions and 826 deletions
@@ -1,6 +1,6 @@
// Package reference holds the form of an artifact's reference once a registry seat published it: `<seat>/<module>/<artifact>@<digest>`
// (novox/hq ADR 0605 §9), and the older `artifact-store://<path>` form read beside it. It is the Delivery domain's rule over
// the names of the two registry seats that front the artifact store; it names no seat itself (ADR 0634 §4, ADR 0635 §4): the
// (novox/hq ADR 0605 §9), and the older `artifact-store://<path>` form read beside it. It is the rule the holders and callers of the two
// registry seats that front the artifact store share (the seats' client, novox/hq ADR 0625 §4, ADR 0634 §2); it names no seat itself (ADR 0634 §4, ADR 0635 §4): the
// caller gives the seats' names from their contracts (Seats), so a package here cannot drift from a contract.
package reference
@@ -7,7 +7,7 @@ import (
var digest = "sha256:" + strings.Repeat("d", 64)
var seats = Seats{Image: "container-image-registry", Object: "oci-artifact-registry"}
var seats = Seats{Image: "container-image-registry", Object: "object-registry"}
func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) {
cases := map[string]string{
@@ -21,7 +21,7 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) {
"https://registry.example/v2/gitea/config/blobs/" + digest: "https://registry.example/v2/gitea/config/blobs/" + digest,
// The form a registry seat answers is recorded as it is (ADR 0605 §9).
"container-image-registry/gitea/server@" + digest: "container-image-registry/gitea/server@" + digest,
"oci-artifact-registry/gitea/config@" + digest: "oci-artifact-registry/gitea/config@" + digest,
"object-registry/gitea/config@" + digest: "object-registry/gitea/config@" + digest,
}
for announced, want := range cases {
if got := seats.Recorded(announced); got != want {
@@ -33,7 +33,7 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) {
ArtifactStoreScheme + "gitea/server@" + digest: "10.0.0.1:5000/gitea/server@" + digest,
"container-image-registry/gitea/server@" + digest: "10.0.0.1:5000/gitea/server@" + digest,
ArtifactStoreScheme + "gitea/config/blobs/" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest,
"oci-artifact-registry/gitea/config@" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest,
"object-registry/gitea/config@" + digest: "http://10.0.0.1:5000/v2/gitea/config/blobs/" + digest,
"registry.example/gitea/server@" + digest: "registry.example/gitea/server@" + digest,
"mesh-artifact-store/gitea/server@" + digest: "mesh-artifact-store/gitea/server@" + digest,
// An upstream copy under the holder's own naming, in both forms.
@@ -54,7 +54,7 @@ func TestAReferenceIsRecordedWithoutTheStoresAddress(t *testing.T) {
// The holder's naming of an upstream copy reads through the image seat only, and InUpstream reads its host and path.
for reference, want := range map[string]bool{
"container-image-registry/upstream/quay.io:443/org/app@" + digest: true,
"oci-artifact-registry/upstream/quay.io:443/org/app@" + digest: false,
"object-registry/upstream/quay.io:443/org/app@" + digest: false,
"container-image-registry/upstream/org/app@" + digest: false,
"container-image-registry/upstream/quay.io/app": false,
} {
@@ -1,11 +1,14 @@
// Package fingerprint holds the form of a module's source fingerprint, `src2:` and 64 lowercase hex digits (novox/hq ADR 0593
// §2): the indexer computes it alone, in its own package; every part that keys a state by it reads this form. It is the
// Delivery domain's rule, over the `fp2` field of mesh-catalog's `modules` value (ADR 0634 §1).
package fingerprint
// Package meshcatalogclient holds the logic the holders and callers of the seat mesh-catalog share over its contract (novox/hq
// ADR 0625 §4, ADR 0634 §2): the form of a module's source fingerprint, `src2:` and 64 lowercase hex digits (ADR 0593 §2),
// over the `fp2` field of the seat's `modules` value. The indexer computes a fingerprint alone, in its own package; every part
// that keys a state by it, the node-engine among them, reads this form here.
package meshcatalogclient
import (
"fmt"
"strings"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// SourcePrefix2 opens every source fingerprint of the form.
@@ -23,7 +26,7 @@ type Toolchain2 struct {
// refuses a value that is not `src2:` and 64 lowercase hex digits, a src1 fingerprint included, naming it.
func KeyPart(fp string) (string, error) {
part, ok := strings.CutPrefix(fp, SourcePrefix2)
if !ok || !isHex64(part) {
if !ok || !keys.IsHex64(part) {
return "", fmt.Errorf("fingerprint: %q is not a fingerprint of the form, %s and 64 lowercase hex digits", fp, SourcePrefix2)
}
return part, nil
@@ -31,12 +34,8 @@ func KeyPart(fp string) (string, error) {
// ParseKeyPart is the inverse of KeyPart. It refuses a part that is not 64 lowercase hex digits, naming it.
func ParseKeyPart(part string) (string, error) {
if !isHex64(part) {
if !keys.IsHex64(part) {
return "", fmt.Errorf("fingerprint: %q is not the key part of a fingerprint, 64 lowercase hex digits", part)
}
return SourcePrefix2 + part, nil
}
func isHex64(s string) bool {
return len(s) == 64 && strings.Trim(s, "0123456789abcdef") == ""
}
@@ -1,4 +1,4 @@
package fingerprint
package meshcatalogclient
import (
"strings"
@@ -0,0 +1,14 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
@@ -0,0 +1,18 @@
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000 h1:BTqpa7dCCTQH9m08Jle8KVFwyIHiHPSUbwWxuqSg2yA=
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
@@ -6,10 +6,11 @@ package meshdeliverycheckerclient
import (
"fmt"
pathpkg "path"
"strings"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/kept"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The states of a verdict, the levels of a check and the compositions' states.
@@ -44,10 +45,10 @@ func CheckRepository(repository string) error {
if !ok || strings.Contains(name, "/") {
return fmt.Errorf("meshdeliverycheckerclient: the repository %q is not <owner>/<name>, with exactly one slash", repository)
}
if err := kept.CheckName("repository owner", owner); err != nil {
if err := keys.CheckName("repository owner", owner); err != nil {
return fmt.Errorf("meshdeliverycheckerclient: the repository %q: %w", repository, err)
}
if err := kept.CheckName("repository name", name); err != nil {
if err := keys.CheckName("repository name", name); err != nil {
return fmt.Errorf("meshdeliverycheckerclient: the repository %q: %w", repository, err)
}
return nil
@@ -220,3 +221,60 @@ func CheckKept(k checker.KeptByLevelDigestValue) error {
}
return nil
}
// CheckPreview refuses a preview a reader could not place (the checker writes it, the indexer reads it for the candidates'
// fingerprints): a repository, head or trunk commit that does not key, a head in conflict that names no file or has a merge
// commit or modules, a head that merged and names no merge commit, and a module with no directory, no tree, a manifest
// digest that is not 64 lowercase hex digits, a lock without path or hash, or a named path that is not clean and inside the
// module's directory or names no git object.
func CheckPreview(p checker.PreviewsByRepositoryHeadTrunkValue) error {
const who = "meshdeliverycheckerclient: "
if err := CheckRepository(p.Repository); err != nil {
return err
}
if err := keys.CheckName("head", p.Head); err != nil {
return fmt.Errorf(who+"the preview: %w", err)
}
if err := keys.CheckName("trunk commit", p.Trunk); err != nil {
return fmt.Errorf(who+"the preview: %w", err)
}
for _, f := range p.Conflicts {
if f == "" {
return fmt.Errorf(who + "a preview names a conflicting file with an empty path")
}
}
if len(p.Conflicts) > 0 {
if p.Merge != "" || len(p.Modules) > 0 {
return fmt.Errorf(who + "a preview of a head in conflict has no merge commit and no modules")
}
return nil
}
if p.Merge == "" {
return fmt.Errorf(who + "a preview names no merge commit")
}
for dir, m := range p.Modules {
if dir == "" {
return fmt.Errorf(who + "a preview names a module with an empty directory")
}
if m.Tree == "" {
return fmt.Errorf(who+"the preview's module %q has no tree", dir)
}
if !keys.IsHex64(m.Manifest) {
return fmt.Errorf(who+"the preview's module %q: %q is not a manifest digest, 64 lowercase hex digits", dir, m.Manifest)
}
for path, hash := range m.Locks {
if path == "" || hash == "" {
return fmt.Errorf(who+"the lock %q of the preview's module %q has an empty path or hash", path, dir)
}
}
for p, object := range m.Paths {
if p == "" || p != pathpkg.Clean(p) || strings.HasPrefix(p, "/") || p == ".." || strings.HasPrefix(p, "../") {
return fmt.Errorf(who+"the preview's module %q: the named path %q is not a clean path inside the module's directory", dir, p)
}
if object == "" {
return fmt.Errorf(who+"the preview's module %q: the named path %q names no git object", dir, p)
}
}
}
return nil
}
@@ -59,6 +59,7 @@ func TestAVerdictIsRefusedForWhatAReaderCouldNotPlace(t *testing.T) {
}
func TestAKeptVerdictIsAPassOfAPackageAtALevel(t *testing.T) {
t.Run("a preview is refused for what a reader of candidates could not place", previewIsRefused)
ok := checker.KeptByLevelDigestValue{State: VerdictPass, Repository: "novox/mesh-sdk", Package: "./go/keys", Level: LevelUnit, Toolchain: "1.26.0", At: "2026-11-05T10:00:00Z"}
if err := CheckKept(ok); err != nil {
t.Fatalf("a kept pass is refused: %v", err)
@@ -79,3 +80,48 @@ func TestAKeptVerdictIsAPassOfAPackageAtALevel(t *testing.T) {
}
}
}
// A preview is refused for what a reader of candidates could not place.
func previewIsRefused(t *testing.T) {
good := func() checker.PreviewsByRepositoryHeadTrunkValue {
return checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h1", Trunk: "t1", Merge: "m1",
Modules: map[string]checker.PreviewsByRepositoryHeadTrunkValueModulesValue{"mod": {Tree: "tr", Manifest: strings.Repeat("a", 64),
Locks: map[string]string{"go.sum": "x"}, Paths: map[string]string{"src": "o1"}}}}
}
if err := CheckPreview(good()); err != nil {
t.Fatalf("a good preview is refused: %v", err)
}
for name, c := range map[string]struct {
edit func(*checker.PreviewsByRepositoryHeadTrunkValue)
says string
}{
"a repository with no owner": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Repository = "a" }, "<owner>/<name>"},
"a head with a dot": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Head = "h.1" }, "head"},
"a conflict with a merge": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Conflicts = []string{"f"} }, "in conflict"},
"no merge commit": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) { p.Merge = "" }, "no merge commit"},
"a module with no tree": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) {
m := p.Modules["mod"]
m.Tree = ""
p.Modules["mod"] = m
}, "no tree"},
"a short manifest digest": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) {
m := p.Modules["mod"]
m.Manifest = "abc"
p.Modules["mod"] = m
}, "manifest digest"},
"a path outside the module": {func(p *checker.PreviewsByRepositoryHeadTrunkValue) {
m := p.Modules["mod"]
m.Paths = map[string]string{"../x": "o"}
p.Modules["mod"] = m
}, "clean path"},
} {
p := good()
c.edit(&p)
if err := CheckPreview(p); err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: CheckPreview = %v, want it to say %q", name, err, c.says)
}
}
if err := CheckPreview(checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h", Trunk: "t", Conflicts: []string{"f"}}); err != nil {
t.Errorf("a head in conflict is refused: %v", err)
}
}
@@ -0,0 +1,323 @@
// Package meshdeliveryclient holds the logic the holders and callers of the seat mesh-delivery share over its contract
// (novox/hq ADR 0625 §4, ADR 0634 §2, ADR 0635 §5): the keys of its states `merges`, `asks` and `owed` and the rules their
// values keep, the id the teller gives the router for an ask, and the act an ask's options bind. The gate writes them, the
// forge bridge, the teller and the assigner read them; the contract's decoding is strict by schema only, so these rules are
// written once here and no caller copies them.
package meshdeliveryclient
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// AskKindMerge is the kind of the gate's ask for a group's merge, the first part of its key.
const AskKindMerge = "merge"
// VerbMerge is the act an ask for a group's merge binds in its options, `mesh-delivery.merge` (Approve).
const VerbMerge = "merge"
// Head is one pull request at one commit: the repository as `<owner>/<name>` and the full sha.
type Head struct {
Repository string `json:"repository"`
Number int `json:"number"`
Head string `json:"head"`
}
// HeadsHash is the first 12 hex digits of the sha256 over the lines `<repository>#<number>=<head>`, sorted and joined by a
// newline: the same for every order of heads, different when any head changes. An empty list is refused.
func HeadsHash(heads []Head) (string, error) {
if len(heads) == 0 {
return "", fmt.Errorf("meshdeliveryclient: a heads hash needs at least one pull request")
}
lines := make([]string, len(heads))
for i, h := range heads {
lines[i] = fmt.Sprintf("%s#%d=%s", h.Repository, h.Number, h.Head)
}
sort.Strings(lines)
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
return hex.EncodeToString(sum[:])[:12], nil
}
var hashOK = regexp.MustCompile(`^[0-9a-f]{12}$`)
func groupKey(group string) (string, error) {
if group == "" {
return "", fmt.Errorf("meshdeliveryclient: the group name is empty")
}
return keys.EscapeName(group), nil
}
// MergesKey is the key, in the state merges, of a group: `<group>`, escaped so a branch such as `fix/1.2` keys.
func MergesKey(group string) (string, error) { return groupKey(group) }
// MergeAskKey is the key, in the state asks, of the ask for merging a group at these heads: `merge.<group>.<heads hash>`.
// The teller's answer has the same key.
func MergeAskKey(group string, heads []Head) (string, error) {
part, err := groupKey(group)
if err != nil {
return "", err
}
hash, err := HeadsHash(heads)
if err != nil {
return "", err
}
return AskKindMerge + "." + part + "." + hash, nil
}
// ParseAskKey is the inverse of MergeAskKey: the kind, the group and the heads hash.
func ParseAskKey(key string) (kind, group, hash string, err error) {
parts := strings.Split(key, ".")
if len(parts) != 3 || parts[0] != AskKindMerge {
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key, merge.<group>.<heads hash>", key)
}
if parts[1] == "" {
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: the group name is empty", key)
}
if group, err = keys.UnescapeName(parts[1]); err != nil {
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: %w", key, err)
}
if !hashOK.MatchString(parts[2]) {
return "", "", "", fmt.Errorf("meshdeliveryclient: %q is not an ask key: %q is not 12 hex digits", key, parts[2])
}
return parts[0], group, parts[2], nil
}
// SettleGroupPrefix begins the group of a settle ask (novox/hq ADR 0556 clause 5): a person's ask to settle deliveries held
// because their merge reached the trunk without a passing check. It is kept in the asks under MergeAskKey like a merge ask,
// so the teller carries it unchanged. Git forbids a colon in a branch name, so no pull request's group ever begins with it.
const SettleGroupPrefix = "settle:"
// SettleGroup is the group of the settle ask of these delivery ids: the prefix and twelve hex digits of the sha256 over the
// ids, sorted and joined by a newline, so the same deliveries asked again keep one key.
func SettleGroup(ids []string) string {
sorted := append([]string(nil), ids...)
sort.Strings(sorted)
sum := sha256.Sum256([]byte(strings.Join(sorted, "\n")))
return SettleGroupPrefix + hex.EncodeToString(sum[:])[:12]
}
// AskVerb is the act the options of an ask bind, by its group: a settle ask's `mesh-delivery.settle`, any other
// `mesh-delivery.merge`. The gate composes the act with it and the teller checks a warrant against it.
func AskVerb(group string) string {
if strings.HasPrefix(group, SettleGroupPrefix) {
return seat.Seat + "." + seat.VerbSettle
}
return seat.Seat + "." + VerbMerge
}
// AskID is the id the teller gives the router for the ask at askKey. The router's ids refuse dots, so in the readable part a
// dot becomes `-` and any other byte outside letters, digits, `_` and `-` becomes `_`; that part is cut to fit, and every id
// ends in `-` and the first 12 hex digits of the sha256 of the whole key, so two keys never give one id. It cannot be read
// back: the teller keeps the key beside it.
func AskID(askKey string) string {
const most = 64 // the router's bound on an id
const tail = 13 // "-" and 12 hex digits
readable := []byte(askKey)
for i, c := range readable {
switch {
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9':
case c == '.':
readable[i] = '-'
default: // a slash, an equals sign or any other byte the router's id refuses
readable[i] = '_'
}
}
if len(readable) > 0 && (readable[0] == '-' || readable[0] == '_') {
readable[0] = 'k' // an id starts with a letter or digit
}
if len(readable) > most-tail {
readable = readable[:most-tail]
}
sum := sha256.Sum256([]byte(askKey))
return string(readable) + "-" + hex.EncodeToString(sum[:])[:12]
}
// AskingID is the id of the asking-th writing of the ask at askKey. Asking 0 and 1 give AskID(askKey), so the first asking's
// id is unchanged; asking n of 2 or more gives the AskID form of askKey + "." + n, so each asking has an id of its own and the
// router, which drops an ask under an id it keeps as a redelivery, takes each as new and an answer to an earlier asking is
// never taken for a later one.
func AskingID(askKey string, asking int) string {
if asking < 2 {
return AskID(askKey)
}
return AskID(askKey + "." + strconv.Itoa(asking))
}
// The acts the gate owes the forge, the last part of an owed key.
const (
OwedStatus = "status"
OwedView = "view"
OwedNote = "note"
OwedComment = "comment"
)
// The status contexts the mesh owes the forge.
const (
StatusContextMergeGate = "mesh/merge-gate"
StatusContextDelivery = "mesh/delivery"
StatusContextDeliveryGroup = "mesh/delivery-group"
StatusContextRepoCheck = "mesh/repo-check"
StatusContextCheck = "mesh/check"
)
var statusContexts = []string{StatusContextMergeGate, StatusContextDelivery, StatusContextDeliveryGroup, StatusContextRepoCheck, StatusContextCheck}
// StatusContexts is the contexts, in a new slice.
func StatusContexts() []string { return append([]string(nil), statusContexts...) }
// The states a status the gate owes the forge may carry.
var statusStates = []string{"pending", "success", "failure", "error", "warning"}
func checkStatusContext(context string) error {
for _, c := range statusContexts {
if context == c {
return nil
}
}
return fmt.Errorf("meshdeliveryclient: the status context %q is not one of %s", context, strings.Join(statusContexts, ", "))
}
// ParseOwedAct splits an owed act into its kind and, for a status, its context, for a comment its sequence as digits. The act
// is `status=<context>`, `view`, `note` or `comment.<seq>`.
func ParseOwedAct(act string) (kind, context string, err error) {
kind, context, hasContext := strings.Cut(act, "=")
switch kind {
case OwedStatus:
if !hasContext {
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is a status without its context, status=<context>", act)
}
if err := checkStatusContext(context); err != nil {
return "", "", err
}
return kind, context, nil
case OwedView, OwedNote:
if hasContext {
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q takes no context", act)
}
return kind, "", nil
}
if kind, seq, ok := strings.Cut(act, "."); ok && kind == OwedComment {
if n, err := strconv.Atoi(seq); err != nil || n < 1 || strconv.Itoa(n) != seq {
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is a comment without its sequence, comment.<seq> with seq of 1 or more", act)
}
return OwedComment, seq, nil
}
return "", "", fmt.Errorf("meshdeliveryclient: the owed act %q is not status=<context>, view, note or comment.<seq>", act)
}
// OwedCommentAct is the act of the seq-th comment on a head: `comment.<seq>`, seq of 1 or more, so a second comment has a key
// of its own and does not overwrite the first.
func OwedCommentAct(seq int) (string, error) {
if seq < 1 {
return "", fmt.Errorf("meshdeliveryclient: a comment's sequence is 1 or more, not %d", seq)
}
return OwedComment + "." + strconv.Itoa(seq), nil
}
func escapeRepository(repository string) (string, error) {
owner, name, ok := strings.Cut(repository, "/")
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
return "", fmt.Errorf("meshdeliveryclient: the repository %q is not <owner>/<name>, with exactly one slash", repository)
}
return keys.EscapeName(owner) + "/" + keys.EscapeName(name), nil
}
func unescapeRepository(part string) (string, error) {
owner, name, ok := strings.Cut(part, "/")
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
return "", fmt.Errorf("meshdeliveryclient: the repository %q is not <owner>/<name>, with exactly one slash", part)
}
o, err := keys.UnescapeName(owner)
if err != nil {
return "", err
}
n, err := keys.UnescapeName(name)
if err != nil {
return "", err
}
return o + "/" + n, nil
}
// OwedKey is the key, in the state owed, of an act the gate owes the forge on a pull request at a head:
// `<repository>.<number>.<head>.<act>`. The number is 0 for a commit no pull request names, and never negative.
func OwedKey(repository string, number int, head, act string) (string, error) {
repo, err := escapeRepository(repository)
if err != nil {
return "", err
}
if number < 0 {
return "", fmt.Errorf("meshdeliveryclient: an owed key's pull request number is 0 (none) or more, not %d", number)
}
if err := keys.CheckName("head", head); err != nil {
return "", err
}
if _, _, err := ParseOwedAct(act); err != nil {
return "", err
}
return repo + "." + strconv.Itoa(number) + "." + head + "." + act, nil
}
// ParseOwedKey is the inverse of OwedKey; act is whole, as `status=<context>` for a status.
func ParseOwedKey(key string) (repository string, number int, head, act string, err error) {
parts := strings.SplitN(key, ".", 4) // a comment's act holds a dot
if len(parts) != 4 {
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key, <repository>.<number>.<head>.<act>", key)
}
number, convErr := strconv.Atoi(parts[1])
if convErr != nil {
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %q is not a pull request number", key, parts[1])
}
if repository, err = unescapeRepository(parts[0]); err != nil {
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %w", key, err)
}
if _, err := OwedKey(repository, number, parts[2], parts[3]); err != nil {
return "", 0, "", "", fmt.Errorf("meshdeliveryclient: %q is not an owed key: %w", key, err)
}
return repository, number, parts[2], parts[3], nil
}
// CheckOwed refuses an owed value the forge bridge could not perform: a status needs its context (one the mesh owes), a
// state of the list and a target; a view and a comment need text; a note needs text and a ref.
func CheckOwed(o seat.OwedByRepositoryNumberHeadActValue) error {
const who = "meshdeliveryclient: "
switch o.Act {
case OwedStatus:
if o.Context == "" {
return fmt.Errorf(who + "an owed status has no context")
}
if err := checkStatusContext(o.Context); err != nil {
return err
}
if o.Target == "" {
return fmt.Errorf(who + "an owed status has no target")
}
for _, s := range statusStates {
if o.State == s {
return nil
}
}
return fmt.Errorf(who+"the owed status state %q is not %s", o.State, strings.Join(statusStates, ", "))
case OwedView, OwedComment:
if o.Text == "" {
return fmt.Errorf(who+"an owed %s has no text", o.Act)
}
case OwedNote:
if o.Text == "" {
return fmt.Errorf(who + "an owed note has no text")
}
if o.Ref == "" {
return fmt.Errorf(who + "an owed note has no ref")
}
default:
return fmt.Errorf(who+"the owed act %q is not status, view, note or comment", o.Act)
}
return nil
}
@@ -0,0 +1,91 @@
package meshdeliveryclient
import (
"strings"
"testing"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
// The keys, ids and acts equal what the gate and the teller have always written (the expected strings are the trunk's own
// output for the same input), so a value written before is read after.
func TestTheAsksAndOwedActsKeepTheirFormsAndRules(t *testing.T) {
heads := []Head{{Repository: "novox/a", Number: 3, Head: "abc"}, {Repository: "novox/b", Number: 1, Head: "def"}}
key, err := MergeAskKey("fix/1.2", heads)
if err != nil || key != "merge.fix/1=2e2.d106c7950d28" {
t.Fatalf("MergeAskKey = %q, %v", key, err)
}
if rev, _ := MergeAskKey("fix/1.2", []Head{heads[1], heads[0]}); rev != key {
t.Errorf("the heads' order changes the key: %q", rev)
}
if kind, group, hash, err := ParseAskKey(key); err != nil || kind != "merge" || group != "fix/1.2" || hash != "d106c7950d28" {
t.Errorf("ParseAskKey = %q %q %q %v", kind, group, hash, err)
}
for _, bad := range []string{"merge.g", "settle.g.d106c7950d28", "merge..d106c7950d28", "merge.g.D106C7950D28", "merge.g.d106"} {
if _, _, _, err := ParseAskKey(bad); err == nil {
t.Errorf("ParseAskKey(%q) reads a wrong key", bad)
}
}
if _, err := MergeAskKey("", heads); err == nil {
t.Error("an empty group keys")
}
if _, err := MergeAskKey("g", nil); err == nil {
t.Error("no heads keys")
}
if id := AskID(key); id != "merge-fix_1_2e2-d106c7950d28-400030402c88" || AskingID(key, 1) != id || AskingID(key, 0) != id {
t.Errorf("AskID = %q", id)
}
if id := AskingID(key, 3); id != "merge-fix_1_2e2-d106c7950d28-3-ea06879e48d8" || len(id) > 64 {
t.Errorf("AskingID(3) = %q", id)
}
if g := SettleGroup([]string{"b", "a"}); g != "settle:7e18f737311b" || g != SettleGroup([]string{"a", "b"}) {
t.Errorf("SettleGroup = %q", g)
}
if AskVerb(SettleGroup([]string{"a"})) != "mesh-delivery.settle" || AskVerb("fix/1") != "mesh-delivery.merge" {
t.Error("AskVerb binds the wrong act")
}
for act, want := range map[string]string{"status=mesh/check": "novox/a.3.abc.status=mesh/check", "view": "novox/a.3.abc.view", "comment.2": "novox/a.3.abc.comment.2"} {
k, err := OwedKey("novox/a", 3, "abc", act)
if err != nil || k != want {
t.Errorf("OwedKey(%q) = %q, %v", act, k, err)
}
if repo, n, head, a, err := ParseOwedKey(k); err != nil || repo != "novox/a" || n != 3 || head != "abc" || a != act {
t.Errorf("ParseOwedKey(%q) = %q %d %q %q %v", k, repo, n, head, a, err)
}
}
for _, act := range []string{"status", "status=mesh/other", "view=x", "comment", "comment.0", "comment.01", "other"} {
if _, err := OwedKey("novox/a", 3, "abc", act); err == nil {
t.Errorf("OwedKey takes the act %q", act)
}
}
if _, err := OwedKey("novox", 0, "abc", "view"); err == nil {
t.Error("a repository with no slash keys")
}
if _, err := OwedKey("novox/a", -1, "abc", "view"); err == nil {
t.Error("a negative number keys")
}
if act, err := OwedCommentAct(2); err != nil || act != "comment.2" {
t.Errorf("OwedCommentAct = %q, %v", act, err)
}
type owed = seat.OwedByRepositoryNumberHeadActValue
for name, c := range map[string]struct {
v owed
says string
}{
"a good status": {owed{Act: "status", Context: StatusContextCheck, Target: "http://x", State: "success"}, ""},
"a good note": {owed{Act: "note", Text: "t", Ref: "mesh-plan"}, ""},
"a status no target": {owed{Act: "status", Context: StatusContextCheck, State: "success"}, "no target"},
"a status wrong state": {owed{Act: "status", Context: StatusContextCheck, Target: "x", State: "done"}, "state"},
"a status no context": {owed{Act: "status", Target: "x", State: "success"}, "no context"},
"a view no text": {owed{Act: "view"}, "no text"},
"a note no ref": {owed{Act: "note", Text: "t"}, "no ref"},
"an unknown act": {owed{Act: "merge"}, "not status"},
} {
err := CheckOwed(c.v)
if (c.says == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.says)) {
t.Errorf("%s: CheckOwed = %v, want %q", name, err, c.says)
}
}
}
@@ -0,0 +1,17 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef
)
require (
github.com/klauspost/compress v1.20.1 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
@@ -0,0 +1,18 @@
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000 h1:BTqpa7dCCTQH9m08Jle8KVFwyIHiHPSUbwWxuqSg2yA=
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-20261105000000-000000000000/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+130 -96
View File
@@ -187,26 +187,27 @@ func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageVa
return v, nil
}
// BuildAnswerArtifactsItem is a type of the contract.
type BuildAnswerArtifactsItem struct {
Artifact string `json:"artifact"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
}
// BuildAnswerToolchain is a type of the contract.
type BuildAnswerToolchain struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from an upstream pin.
type AskArtifact struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
Seat string `json:"seat,omitempty"`
Version string `json:"version,omitempty"`
}
// BuildAnswer is a type of the contract.
type BuildAnswer struct {
Artifacts []BuildAnswerArtifactsItem `json:"artifacts"`
Imports []string `json:"imports,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *BuildAnswerToolchain `json:"toolchain,omitempty"`
Artifacts []BuiltArtifact `json:"artifacts"`
Imports []string `json:"imports,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// BuiltArtifact is one artifact a build made: its digest and the reference it was published under.
type BuiltArtifact struct {
Artifact string `json:"artifact"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
}
// Capacity is how many steps the node runs at once.
@@ -229,56 +230,10 @@ type Capacity struct {
Why string `json:"why"`
}
// CheckAnswerLevelsItemDigestNeedsItem is a type of the contract.
type CheckAnswerLevelsItemDigestNeedsItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// CheckAnswerLevelsItemDigestPackagesItem is a type of the contract.
type CheckAnswerLevelsItemDigestPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// CheckAnswerLevelsItemDigest is a type of the contract.
type CheckAnswerLevelsItemDigest struct {
Beside []string `json:"beside,omitempty"`
JudgedBy string `json:"judged_by,omitempty"`
Level string `json:"level,omitempty"`
Needs []CheckAnswerLevelsItemDigestNeedsItem `json:"needs,omitempty"`
Packages []CheckAnswerLevelsItemDigestPackagesItem `json:"packages,omitempty"`
WithoutTests []string `json:"without_tests,omitempty"`
}
// CheckAnswerLevelsItemKeptItem is a type of the contract.
type CheckAnswerLevelsItemKeptItem struct {
Digest string `json:"digest,omitempty"`
Package string `json:"package,omitempty"`
Tests int64 `json:"tests,omitempty"`
}
// CheckAnswerLevelsItemUnmetItem is a type of the contract.
type CheckAnswerLevelsItemUnmetItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// CheckAnswerLevelsItem is a type of the contract.
type CheckAnswerLevelsItem struct {
Digest *CheckAnswerLevelsItemDigest `json:"digest,omitempty"`
Judged string `json:"judged,omitempty"`
Kept []CheckAnswerLevelsItemKeptItem `json:"kept,omitempty"`
Level string `json:"level"`
Tests []any `json:"tests,omitempty"`
Unmet []CheckAnswerLevelsItemUnmetItem `json:"unmet,omitempty"`
}
// CheckAnswer is a type of the contract.
type CheckAnswer struct {
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch"`
Levels []CheckAnswerLevelsItem `json:"levels,omitempty"`
Fetch Fetch `json:"fetch"`
Levels []LevelAnswer `json:"levels,omitempty"`
}
// ComposeAnswer is a type of the contract.
@@ -289,6 +244,77 @@ type ComposeAnswer struct {
Verdict any `json:"verdict,omitempty"`
}
// FetchBesideItem is a type of the contract.
type FetchBesideItem struct {
Commit string `json:"commit"`
Repository string `json:"repository"`
}
// Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside.
type Fetch struct {
// the trunk commit a merge preview merges onto
Base string `json:"base,omitempty"`
// the sibling repositories placed beside the fetched tree
Beside []FetchBesideItem `json:"beside,omitempty"`
// the full commit
Commit string `json:"commit"`
// the module's directory in the repository ("." for its root), with paths
Dir string `json:"dir,omitempty"`
// the version (a full commit) of each mesh package to build against, by its module path
Packages map[string]string `json:"packages,omitempty"`
// the paths the module names, relative to dir
Paths []string `json:"paths,omitempty"`
// the pull request's ref, when only its head carries the commit
Ref string `json:"ref,omitempty"`
// <owner>/<name>
Repository string `json:"repository"`
}
// LevelAnswerDigestNeedsItem is a type of the contract.
type LevelAnswerDigestNeedsItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswerDigestPackagesItem is a type of the contract.
type LevelAnswerDigestPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// LevelAnswerDigest is a type of the contract.
type LevelAnswerDigest struct {
Beside []string `json:"beside,omitempty"`
JudgedBy string `json:"judged_by,omitempty"`
Level string `json:"level,omitempty"`
Needs []LevelAnswerDigestNeedsItem `json:"needs,omitempty"`
Packages []LevelAnswerDigestPackagesItem `json:"packages,omitempty"`
WithoutTests []string `json:"without_tests,omitempty"`
}
// LevelAnswerKeptItem is a type of the contract.
type LevelAnswerKeptItem struct {
Digest string `json:"digest,omitempty"`
Package string `json:"package,omitempty"`
Tests int64 `json:"tests,omitempty"`
}
// LevelAnswerUnmetItem is a type of the contract.
type LevelAnswerUnmetItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswer is one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test.
type LevelAnswer struct {
Digest *LevelAnswerDigest `json:"digest,omitempty"`
Judged string `json:"judged,omitempty"`
Kept []LevelAnswerKeptItem `json:"kept,omitempty"`
Level string `json:"level"`
Tests []any `json:"tests,omitempty"`
Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"`
}
// Pause is whether the node takes no new ask.
type Pause struct {
// when (RFC 3339 time)
@@ -308,10 +334,9 @@ type Step struct {
Owner string `json:"owner,omitempty"`
Phase string `json:"phase,omitempty"`
// when (RFC 3339 time)
PhaseAt string `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
// what the step answered
Result any `json:"result,omitempty"`
PhaseAt string `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
Result *StepResult `json:"result,omitempty"`
// when (RFC 3339 time)
Started string `json:"started"`
// running, passed, failed or refused
@@ -322,37 +347,20 @@ type Step struct {
What string `json:"what,omitempty"`
}
// StepAskBuildArtifactsItem is a type of the contract.
type StepAskBuildArtifactsItem struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
Seat string `json:"seat,omitempty"`
Version string `json:"version,omitempty"`
}
// StepAskBuildToolchain is a type of the contract.
type StepAskBuildToolchain struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
}
// StepAskBuild is a type of the contract.
type StepAskBuild struct {
Artifacts []StepAskBuildArtifactsItem `json:"artifacts"`
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch,omitempty"`
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
Path string `json:"path,omitempty"`
Publish bool `json:"publish,omitempty"`
Toolchain *StepAskBuildToolchain `json:"toolchain,omitempty"`
Artifacts []AskArtifact `json:"artifacts"`
Fetch *Fetch `json:"fetch,omitempty"`
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
Path string `json:"path,omitempty"`
Publish bool `json:"publish,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// StepAskCheck is a type of the contract.
type StepAskCheck struct {
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch"`
Fetch Fetch `json:"fetch"`
Kept bool `json:"kept,omitempty"`
Levels []string `json:"levels"`
Packages []string `json:"packages,omitempty"`
@@ -363,9 +371,8 @@ type StepAskCheck struct {
// StepAskCompose is a type of the contract.
type StepAskCompose struct {
Dir string `json:"dir"`
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch"`
Dir string `json:"dir"`
Fetch Fetch `json:"fetch"`
Package string `json:"package"`
Reader string `json:"reader,omitempty"`
// the compose-candidate request
@@ -392,6 +399,26 @@ type StepAsk struct {
V int64 `json:"v,omitempty"`
}
// StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four.
type StepResult struct {
Artifacts []BuiltArtifact `json:"artifacts,omitempty"`
Built bool `json:"built,omitempty"`
// the holder declined the ask: its node lacks what the ask needs
Declined bool `json:"declined,omitempty"`
Fetch *Fetch `json:"fetch,omitempty"`
Imports []string `json:"imports,omitempty"`
Key string `json:"key,omitempty"`
// what the holder's node lacks of it
Lacks []string `json:"lacks,omitempty"`
Levels []LevelAnswer `json:"levels,omitempty"`
// what the declined ask needs
Needs []string `json:"needs,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
// the compose-candidate verdict (the composer's own layout)
Verdict any `json:"verdict,omitempty"`
}
// Toolchain is the toolchain a node reports.
type Toolchain struct {
// when (RFC 3339 time)
@@ -404,6 +431,13 @@ type Toolchain struct {
Version string `json:"version"`
}
// ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe.
type ToolchainUsed struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
}
// CurrentInput is what current takes.
type CurrentInput struct {
}
@@ -39,6 +39,14 @@ func TestFixtures(t *testing.T) {
}, subjectIs: nil},
{name: "a build ask", subject: "mesh.seat.node-build-go.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}",
decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.delivery$")},
{name: "a check ask with its fetch, siblings and levels", subject: "mesh.seat.node-build-go.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"]},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"]},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}",
decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.check$")},
{name: "a finished build step with its artifacts", subject: "", given: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a finished check step with its levels", subject: "", given: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-catalog\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:06:00Z\",\"id\":\"c3d4\",\"node\":\"ace\",\"result\":{\"fetch\":{\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"repository\":\"novox/mesh-catalog\"},\"levels\":[{\"digest\":{\"level\":\"unit\",\"packages\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\"}]},\"kept\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"package\":\"./modules/a\",\"tests\":12}],\"level\":\"unit\",\"tests\":[{\"package\":\"./modules/b\",\"pass\":true}],\"unmet\":[{\"needs\":[\"docker\"],\"package\":\"./modules/c\"}]}]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a declined step", subject: "", given: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}", wire: "{\"id\":\"c3d5\",\"node\":\"ace\",\"result\":{\"declined\":true,\"lacks\":[\"docker\"],\"needs\":[\"docker\"]},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"refused\",\"tool\":\"check\",\"v\":1}",
decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
}
for _, c := range cases {
v, err := c.decode([]byte(c.given))
+130 -96
View File
@@ -187,26 +187,27 @@ func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageVa
return v, nil
}
// BuildAnswerArtifactsItem is a type of the contract.
type BuildAnswerArtifactsItem struct {
Artifact string `json:"artifact"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
}
// BuildAnswerToolchain is a type of the contract.
type BuildAnswerToolchain struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from an upstream pin.
type AskArtifact struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
Seat string `json:"seat,omitempty"`
Version string `json:"version,omitempty"`
}
// BuildAnswer is a type of the contract.
type BuildAnswer struct {
Artifacts []BuildAnswerArtifactsItem `json:"artifacts"`
Imports []string `json:"imports,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *BuildAnswerToolchain `json:"toolchain,omitempty"`
Artifacts []BuiltArtifact `json:"artifacts"`
Imports []string `json:"imports,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// BuiltArtifact is one artifact a build made: its digest and the reference it was published under.
type BuiltArtifact struct {
Artifact string `json:"artifact"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
}
// Capacity is how many steps the node runs at once.
@@ -229,56 +230,10 @@ type Capacity struct {
Why string `json:"why"`
}
// CheckAnswerLevelsItemDigestNeedsItem is a type of the contract.
type CheckAnswerLevelsItemDigestNeedsItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// CheckAnswerLevelsItemDigestPackagesItem is a type of the contract.
type CheckAnswerLevelsItemDigestPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// CheckAnswerLevelsItemDigest is a type of the contract.
type CheckAnswerLevelsItemDigest struct {
Beside []string `json:"beside,omitempty"`
JudgedBy string `json:"judged_by,omitempty"`
Level string `json:"level,omitempty"`
Needs []CheckAnswerLevelsItemDigestNeedsItem `json:"needs,omitempty"`
Packages []CheckAnswerLevelsItemDigestPackagesItem `json:"packages,omitempty"`
WithoutTests []string `json:"without_tests,omitempty"`
}
// CheckAnswerLevelsItemKeptItem is a type of the contract.
type CheckAnswerLevelsItemKeptItem struct {
Digest string `json:"digest,omitempty"`
Package string `json:"package,omitempty"`
Tests int64 `json:"tests,omitempty"`
}
// CheckAnswerLevelsItemUnmetItem is a type of the contract.
type CheckAnswerLevelsItemUnmetItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// CheckAnswerLevelsItem is a type of the contract.
type CheckAnswerLevelsItem struct {
Digest *CheckAnswerLevelsItemDigest `json:"digest,omitempty"`
Judged string `json:"judged,omitempty"`
Kept []CheckAnswerLevelsItemKeptItem `json:"kept,omitempty"`
Level string `json:"level"`
Tests []any `json:"tests,omitempty"`
Unmet []CheckAnswerLevelsItemUnmetItem `json:"unmet,omitempty"`
}
// CheckAnswer is a type of the contract.
type CheckAnswer struct {
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch"`
Levels []CheckAnswerLevelsItem `json:"levels,omitempty"`
Fetch Fetch `json:"fetch"`
Levels []LevelAnswer `json:"levels,omitempty"`
}
// ComposeAnswer is a type of the contract.
@@ -289,6 +244,77 @@ type ComposeAnswer struct {
Verdict any `json:"verdict,omitempty"`
}
// FetchBesideItem is a type of the contract.
type FetchBesideItem struct {
Commit string `json:"commit"`
Repository string `json:"repository"`
}
// Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside.
type Fetch struct {
// the trunk commit a merge preview merges onto
Base string `json:"base,omitempty"`
// the sibling repositories placed beside the fetched tree
Beside []FetchBesideItem `json:"beside,omitempty"`
// the full commit
Commit string `json:"commit"`
// the module's directory in the repository ("." for its root), with paths
Dir string `json:"dir,omitempty"`
// the version (a full commit) of each mesh package to build against, by its module path
Packages map[string]string `json:"packages,omitempty"`
// the paths the module names, relative to dir
Paths []string `json:"paths,omitempty"`
// the pull request's ref, when only its head carries the commit
Ref string `json:"ref,omitempty"`
// <owner>/<name>
Repository string `json:"repository"`
}
// LevelAnswerDigestNeedsItem is a type of the contract.
type LevelAnswerDigestNeedsItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswerDigestPackagesItem is a type of the contract.
type LevelAnswerDigestPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// LevelAnswerDigest is a type of the contract.
type LevelAnswerDigest struct {
Beside []string `json:"beside,omitempty"`
JudgedBy string `json:"judged_by,omitempty"`
Level string `json:"level,omitempty"`
Needs []LevelAnswerDigestNeedsItem `json:"needs,omitempty"`
Packages []LevelAnswerDigestPackagesItem `json:"packages,omitempty"`
WithoutTests []string `json:"without_tests,omitempty"`
}
// LevelAnswerKeptItem is a type of the contract.
type LevelAnswerKeptItem struct {
Digest string `json:"digest,omitempty"`
Package string `json:"package,omitempty"`
Tests int64 `json:"tests,omitempty"`
}
// LevelAnswerUnmetItem is a type of the contract.
type LevelAnswerUnmetItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswer is one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test.
type LevelAnswer struct {
Digest *LevelAnswerDigest `json:"digest,omitempty"`
Judged string `json:"judged,omitempty"`
Kept []LevelAnswerKeptItem `json:"kept,omitempty"`
Level string `json:"level"`
Tests []any `json:"tests,omitempty"`
Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"`
}
// Pause is whether the node takes no new ask.
type Pause struct {
// when (RFC 3339 time)
@@ -308,10 +334,9 @@ type Step struct {
Owner string `json:"owner,omitempty"`
Phase string `json:"phase,omitempty"`
// when (RFC 3339 time)
PhaseAt string `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
// what the step answered
Result any `json:"result,omitempty"`
PhaseAt string `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
Result *StepResult `json:"result,omitempty"`
// when (RFC 3339 time)
Started string `json:"started"`
// running, passed, failed or refused
@@ -322,37 +347,20 @@ type Step struct {
What string `json:"what,omitempty"`
}
// StepAskBuildArtifactsItem is a type of the contract.
type StepAskBuildArtifactsItem struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
Seat string `json:"seat,omitempty"`
Version string `json:"version,omitempty"`
}
// StepAskBuildToolchain is a type of the contract.
type StepAskBuildToolchain struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
}
// StepAskBuild is a type of the contract.
type StepAskBuild struct {
Artifacts []StepAskBuildArtifactsItem `json:"artifacts"`
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch,omitempty"`
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
Path string `json:"path,omitempty"`
Publish bool `json:"publish,omitempty"`
Toolchain *StepAskBuildToolchain `json:"toolchain,omitempty"`
Artifacts []AskArtifact `json:"artifacts"`
Fetch *Fetch `json:"fetch,omitempty"`
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
Path string `json:"path,omitempty"`
Publish bool `json:"publish,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// StepAskCheck is a type of the contract.
type StepAskCheck struct {
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch"`
Fetch Fetch `json:"fetch"`
Kept bool `json:"kept,omitempty"`
Levels []string `json:"levels"`
Packages []string `json:"packages,omitempty"`
@@ -363,9 +371,8 @@ type StepAskCheck struct {
// StepAskCompose is a type of the contract.
type StepAskCompose struct {
Dir string `json:"dir"`
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch"`
Dir string `json:"dir"`
Fetch Fetch `json:"fetch"`
Package string `json:"package"`
Reader string `json:"reader,omitempty"`
// the compose-candidate request
@@ -392,6 +399,26 @@ type StepAsk struct {
V int64 `json:"v,omitempty"`
}
// StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four.
type StepResult struct {
Artifacts []BuiltArtifact `json:"artifacts,omitempty"`
Built bool `json:"built,omitempty"`
// the holder declined the ask: its node lacks what the ask needs
Declined bool `json:"declined,omitempty"`
Fetch *Fetch `json:"fetch,omitempty"`
Imports []string `json:"imports,omitempty"`
Key string `json:"key,omitempty"`
// what the holder's node lacks of it
Lacks []string `json:"lacks,omitempty"`
Levels []LevelAnswer `json:"levels,omitempty"`
// what the declined ask needs
Needs []string `json:"needs,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
// the compose-candidate verdict (the composer's own layout)
Verdict any `json:"verdict,omitempty"`
}
// Toolchain is the toolchain a node reports.
type Toolchain struct {
// when (RFC 3339 time)
@@ -404,6 +431,13 @@ type Toolchain struct {
Version string `json:"version"`
}
// ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe.
type ToolchainUsed struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
}
// CurrentInput is what current takes.
type CurrentInput struct {
}
+130 -96
View File
@@ -187,26 +187,27 @@ func DecodeToolchainsByNodeLanguageValue(raw []byte) (ToolchainsByNodeLanguageVa
return v, nil
}
// BuildAnswerArtifactsItem is a type of the contract.
type BuildAnswerArtifactsItem struct {
Artifact string `json:"artifact"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
}
// BuildAnswerToolchain is a type of the contract.
type BuildAnswerToolchain struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
// AskArtifact is one artifact of a build ask: built from the fetched tree, or copied from an upstream pin.
type AskArtifact struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
Seat string `json:"seat,omitempty"`
Version string `json:"version,omitempty"`
}
// BuildAnswer is a type of the contract.
type BuildAnswer struct {
Artifacts []BuildAnswerArtifactsItem `json:"artifacts"`
Imports []string `json:"imports,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *BuildAnswerToolchain `json:"toolchain,omitempty"`
Artifacts []BuiltArtifact `json:"artifacts"`
Imports []string `json:"imports,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// BuiltArtifact is one artifact a build made: its digest and the reference it was published under.
type BuiltArtifact struct {
Artifact string `json:"artifact"`
Digest string `json:"digest"`
Reference string `json:"reference,omitempty"`
}
// Capacity is how many steps the node runs at once.
@@ -229,56 +230,10 @@ type Capacity struct {
Why string `json:"why"`
}
// CheckAnswerLevelsItemDigestNeedsItem is a type of the contract.
type CheckAnswerLevelsItemDigestNeedsItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// CheckAnswerLevelsItemDigestPackagesItem is a type of the contract.
type CheckAnswerLevelsItemDigestPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// CheckAnswerLevelsItemDigest is a type of the contract.
type CheckAnswerLevelsItemDigest struct {
Beside []string `json:"beside,omitempty"`
JudgedBy string `json:"judged_by,omitempty"`
Level string `json:"level,omitempty"`
Needs []CheckAnswerLevelsItemDigestNeedsItem `json:"needs,omitempty"`
Packages []CheckAnswerLevelsItemDigestPackagesItem `json:"packages,omitempty"`
WithoutTests []string `json:"without_tests,omitempty"`
}
// CheckAnswerLevelsItemKeptItem is a type of the contract.
type CheckAnswerLevelsItemKeptItem struct {
Digest string `json:"digest,omitempty"`
Package string `json:"package,omitempty"`
Tests int64 `json:"tests,omitempty"`
}
// CheckAnswerLevelsItemUnmetItem is a type of the contract.
type CheckAnswerLevelsItemUnmetItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// CheckAnswerLevelsItem is a type of the contract.
type CheckAnswerLevelsItem struct {
Digest *CheckAnswerLevelsItemDigest `json:"digest,omitempty"`
Judged string `json:"judged,omitempty"`
Kept []CheckAnswerLevelsItemKeptItem `json:"kept,omitempty"`
Level string `json:"level"`
Tests []any `json:"tests,omitempty"`
Unmet []CheckAnswerLevelsItemUnmetItem `json:"unmet,omitempty"`
}
// CheckAnswer is a type of the contract.
type CheckAnswer struct {
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch"`
Levels []CheckAnswerLevelsItem `json:"levels,omitempty"`
Fetch Fetch `json:"fetch"`
Levels []LevelAnswer `json:"levels,omitempty"`
}
// ComposeAnswer is a type of the contract.
@@ -289,6 +244,77 @@ type ComposeAnswer struct {
Verdict any `json:"verdict,omitempty"`
}
// FetchBesideItem is a type of the contract.
type FetchBesideItem struct {
Commit string `json:"commit"`
Repository string `json:"repository"`
}
// Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside.
type Fetch struct {
// the trunk commit a merge preview merges onto
Base string `json:"base,omitempty"`
// the sibling repositories placed beside the fetched tree
Beside []FetchBesideItem `json:"beside,omitempty"`
// the full commit
Commit string `json:"commit"`
// the module's directory in the repository ("." for its root), with paths
Dir string `json:"dir,omitempty"`
// the version (a full commit) of each mesh package to build against, by its module path
Packages map[string]string `json:"packages,omitempty"`
// the paths the module names, relative to dir
Paths []string `json:"paths,omitempty"`
// the pull request's ref, when only its head carries the commit
Ref string `json:"ref,omitempty"`
// <owner>/<name>
Repository string `json:"repository"`
}
// LevelAnswerDigestNeedsItem is a type of the contract.
type LevelAnswerDigestNeedsItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswerDigestPackagesItem is a type of the contract.
type LevelAnswerDigestPackagesItem struct {
Digest string `json:"digest"`
Package string `json:"package"`
}
// LevelAnswerDigest is a type of the contract.
type LevelAnswerDigest struct {
Beside []string `json:"beside,omitempty"`
JudgedBy string `json:"judged_by,omitempty"`
Level string `json:"level,omitempty"`
Needs []LevelAnswerDigestNeedsItem `json:"needs,omitempty"`
Packages []LevelAnswerDigestPackagesItem `json:"packages,omitempty"`
WithoutTests []string `json:"without_tests,omitempty"`
}
// LevelAnswerKeptItem is a type of the contract.
type LevelAnswerKeptItem struct {
Digest string `json:"digest,omitempty"`
Package string `json:"package,omitempty"`
Tests int64 `json:"tests,omitempty"`
}
// LevelAnswerUnmetItem is a type of the contract.
type LevelAnswerUnmetItem struct {
Needs []string `json:"needs,omitempty"`
Package string `json:"package,omitempty"`
}
// LevelAnswer is one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test.
type LevelAnswer struct {
Digest *LevelAnswerDigest `json:"digest,omitempty"`
Judged string `json:"judged,omitempty"`
Kept []LevelAnswerKeptItem `json:"kept,omitempty"`
Level string `json:"level"`
Tests []any `json:"tests,omitempty"`
Unmet []LevelAnswerUnmetItem `json:"unmet,omitempty"`
}
// Pause is whether the node takes no new ask.
type Pause struct {
// when (RFC 3339 time)
@@ -308,10 +334,9 @@ type Step struct {
Owner string `json:"owner,omitempty"`
Phase string `json:"phase,omitempty"`
// when (RFC 3339 time)
PhaseAt string `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
// what the step answered
Result any `json:"result,omitempty"`
PhaseAt string `json:"phase-at,omitempty"`
Reason string `json:"reason,omitempty"`
Result *StepResult `json:"result,omitempty"`
// when (RFC 3339 time)
Started string `json:"started"`
// running, passed, failed or refused
@@ -322,37 +347,20 @@ type Step struct {
What string `json:"what,omitempty"`
}
// StepAskBuildArtifactsItem is a type of the contract.
type StepAskBuildArtifactsItem struct {
Artifact string `json:"artifact"`
From string `json:"from,omitempty"`
Seat string `json:"seat,omitempty"`
Version string `json:"version,omitempty"`
}
// StepAskBuildToolchain is a type of the contract.
type StepAskBuildToolchain struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
}
// StepAskBuild is a type of the contract.
type StepAskBuild struct {
Artifacts []StepAskBuildArtifactsItem `json:"artifacts"`
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch,omitempty"`
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
Path string `json:"path,omitempty"`
Publish bool `json:"publish,omitempty"`
Toolchain *StepAskBuildToolchain `json:"toolchain,omitempty"`
Artifacts []AskArtifact `json:"artifacts"`
Fetch *Fetch `json:"fetch,omitempty"`
Fp string `json:"fp,omitempty"`
Module string `json:"module"`
Path string `json:"path,omitempty"`
Publish bool `json:"publish,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
}
// StepAskCheck is a type of the contract.
type StepAskCheck struct {
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch"`
Fetch Fetch `json:"fetch"`
Kept bool `json:"kept,omitempty"`
Levels []string `json:"levels"`
Packages []string `json:"packages,omitempty"`
@@ -363,9 +371,8 @@ type StepAskCheck struct {
// StepAskCompose is a type of the contract.
type StepAskCompose struct {
Dir string `json:"dir"`
// what the build module fetches: the repository, commit and directory, as the indexer states it
Fetch any `json:"fetch"`
Dir string `json:"dir"`
Fetch Fetch `json:"fetch"`
Package string `json:"package"`
Reader string `json:"reader,omitempty"`
// the compose-candidate request
@@ -392,6 +399,26 @@ type StepAsk struct {
V int64 `json:"v,omitempty"`
}
// StepResult is what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four.
type StepResult struct {
Artifacts []BuiltArtifact `json:"artifacts,omitempty"`
Built bool `json:"built,omitempty"`
// the holder declined the ask: its node lacks what the ask needs
Declined bool `json:"declined,omitempty"`
Fetch *Fetch `json:"fetch,omitempty"`
Imports []string `json:"imports,omitempty"`
Key string `json:"key,omitempty"`
// what the holder's node lacks of it
Lacks []string `json:"lacks,omitempty"`
Levels []LevelAnswer `json:"levels,omitempty"`
// what the declined ask needs
Needs []string `json:"needs,omitempty"`
OutOfStep []string `json:"out-of-step,omitempty"`
Toolchain *ToolchainUsed `json:"toolchain,omitempty"`
// the compose-candidate verdict (the composer's own layout)
Verdict any `json:"verdict,omitempty"`
}
// Toolchain is the toolchain a node reports.
type Toolchain struct {
// when (RFC 3339 time)
@@ -404,6 +431,13 @@ type Toolchain struct {
Version string `json:"version"`
}
// ToolchainUsed is the toolchain a build was made with: the language, the installed version and the build module's recipe.
type ToolchainUsed struct {
Language string `json:"language"`
Recipe string `json:"recipe"`
Version string `json:"version"`
}
// CurrentInput is what current takes.
type CurrentInput struct {
}
+18
View File
@@ -112,6 +112,24 @@
],
"series": "0.1"
},
{
"name": "go-meshcatalogclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/meshcatalogclient"
],
"series": "0.1"
},
{
"name": "go-meshdeliveryclient",
"kind": "package",
"language": "go",
"paths": [
"go/clients/meshdeliveryclient"
],
"series": "0.1"
},
{
"name": "go-meshdeliverycheckerclient",
"kind": "package",
+323 -190
View File
@@ -64,7 +64,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
},
"build": {
"type": "object",
@@ -79,52 +80,19 @@
"type": "string"
},
"fetch": {
"description": "what the build module fetches: the repository, commit and directory, as the indexer states it"
"$ref": "#/defs/Fetch"
},
"artifacts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"artifact": {
"type": "string"
},
"from": {
"type": "string"
},
"seat": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"artifact"
]
"$ref": "#/defs/AskArtifact"
}
},
"publish": {
"type": "boolean"
},
"toolchain": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"version": {
"type": "string"
},
"recipe": {
"type": "string"
}
},
"required": [
"language",
"version",
"recipe"
]
"$ref": "#/defs/ToolchainUsed"
}
},
"required": [
@@ -136,7 +104,7 @@
"type": "object",
"properties": {
"fetch": {
"description": "what the build module fetches: the repository, commit and directory, as the indexer states it"
"$ref": "#/defs/Fetch"
},
"levels": {
"type": "array",
@@ -173,7 +141,7 @@
"type": "object",
"properties": {
"fetch": {
"description": "what the build module fetches: the repository, commit and directory, as the indexer states it"
"$ref": "#/defs/Fetch"
},
"dir": {
"type": "string"
@@ -223,42 +191,11 @@
"artifacts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"artifact": {
"type": "string"
},
"digest": {
"type": "string"
},
"reference": {
"type": "string"
}
},
"required": [
"artifact",
"digest"
]
"$ref": "#/defs/BuiltArtifact"
}
},
"toolchain": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"version": {
"type": "string"
},
"recipe": {
"type": "string"
}
},
"required": [
"language",
"version",
"recipe"
]
"$ref": "#/defs/ToolchainUsed"
},
"imports": {
"type": "array",
@@ -281,121 +218,12 @@
"type": "object",
"properties": {
"fetch": {
"description": "what the build module fetches: the repository, commit and directory, as the indexer states it"
"$ref": "#/defs/Fetch"
},
"levels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"level": {
"type": "string"
},
"judged": {
"type": "string"
},
"digest": {
"type": "object",
"properties": {
"level": {
"type": "string"
},
"packages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"digest": {
"type": "string"
}
},
"required": [
"package",
"digest"
]
}
},
"beside": {
"type": "array",
"items": {
"type": "string"
}
},
"without_tests": {
"type": "array",
"items": {
"type": "string"
}
},
"judged_by": {
"type": "string"
},
"needs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"needs": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"kept": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"digest": {
"type": "string"
},
"tests": {
"type": "integer"
}
}
}
},
"tests": {
"type": "array",
"items": {
"description": "one test's result"
}
},
"unmet": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"needs": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"required": [
"level"
]
"$ref": "#/defs/LevelAnswer"
}
}
},
@@ -442,17 +270,19 @@
},
"started": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
},
"ended": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
},
"reason": {
"type": "string"
},
"result": {
"description": "what the step answered"
"$ref": "#/defs/StepResult"
},
"what": {
"type": "string"
@@ -462,7 +292,8 @@
},
"phase-at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
},
"owner": {
"type": "string"
@@ -496,7 +327,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
@@ -521,7 +353,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
@@ -574,7 +407,8 @@
},
"at": {
"type": "string",
"description": "when (RFC 3339 time)"
"description": "when (RFC 3339 time)",
"format": "date-time"
}
},
"required": [
@@ -583,6 +417,305 @@
"why"
],
"description": "how many steps the node runs at once"
},
"Fetch": {
"type": "object",
"description": "what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside",
"properties": {
"repository": {
"type": "string",
"description": "<owner>/<name>"
},
"commit": {
"type": "string",
"description": "the full commit"
},
"ref": {
"type": "string",
"description": "the pull request's ref, when only its head carries the commit"
},
"base": {
"type": "string",
"description": "the trunk commit a merge preview merges onto"
},
"dir": {
"type": "string",
"description": "the module's directory in the repository (\".\" for its root), with paths"
},
"paths": {
"type": "array",
"items": {
"type": "string"
},
"description": "the paths the module names, relative to dir"
},
"packages": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "the version (a full commit) of each mesh package to build against, by its module path"
},
"beside": {
"type": "array",
"items": {
"type": "object",
"properties": {
"repository": {
"type": "string"
},
"commit": {
"type": "string"
}
},
"required": [
"repository",
"commit"
]
},
"description": "the sibling repositories placed beside the fetched tree"
}
},
"required": [
"repository",
"commit"
]
},
"ToolchainUsed": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"version": {
"type": "string"
},
"recipe": {
"type": "string"
}
},
"required": [
"language",
"version",
"recipe"
],
"description": "the toolchain a build was made with: the language, the installed version and the build module's recipe"
},
"AskArtifact": {
"type": "object",
"properties": {
"artifact": {
"type": "string"
},
"from": {
"type": "string"
},
"seat": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"artifact"
],
"description": "one artifact of a build ask: built from the fetched tree, or copied from an upstream pin"
},
"BuiltArtifact": {
"type": "object",
"properties": {
"artifact": {
"type": "string"
},
"digest": {
"type": "string"
},
"reference": {
"type": "string"
}
},
"required": [
"artifact",
"digest"
],
"description": "one artifact a build made: its digest and the reference it was published under"
},
"LevelAnswer": {
"type": "object",
"properties": {
"level": {
"type": "string"
},
"judged": {
"type": "string"
},
"digest": {
"type": "object",
"properties": {
"level": {
"type": "string"
},
"packages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"digest": {
"type": "string"
}
},
"required": [
"package",
"digest"
]
}
},
"beside": {
"type": "array",
"items": {
"type": "string"
}
},
"without_tests": {
"type": "array",
"items": {
"type": "string"
}
},
"judged_by": {
"type": "string"
},
"needs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"needs": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"kept": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"digest": {
"type": "string"
},
"tests": {
"type": "integer"
}
}
}
},
"tests": {
"type": "array",
"items": {
"description": "one test's result"
}
},
"unmet": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "string"
},
"needs": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"required": [
"level"
],
"description": "one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test"
},
"StepResult": {
"type": "object",
"description": "what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four",
"properties": {
"artifacts": {
"type": "array",
"items": {
"$ref": "#/defs/BuiltArtifact"
}
},
"toolchain": {
"$ref": "#/defs/ToolchainUsed"
},
"imports": {
"type": "array",
"items": {
"type": "string"
}
},
"out-of-step": {
"type": "array",
"items": {
"type": "string"
}
},
"fetch": {
"$ref": "#/defs/Fetch"
},
"levels": {
"type": "array",
"items": {
"$ref": "#/defs/LevelAnswer"
}
},
"built": {
"type": "boolean"
},
"key": {
"type": "string"
},
"verdict": {
"description": "the compose-candidate verdict (the composer's own layout)"
},
"declined": {
"type": "boolean",
"description": "the holder declined the ask: its node lacks what the ask needs"
},
"needs": {
"type": "array",
"items": {
"type": "string"
},
"description": "what the declined ask needs"
},
"lacks": {
"type": "array",
"items": {
"type": "string"
},
"description": "what the holder's node lacks of it"
}
}
}
}
}
@@ -59,5 +59,145 @@
]
}
}
},
{
"name": "a check ask with its fetch, siblings and levels",
"of": "accept:check",
"subject": "mesh.seat.node-build-go.accept.check",
"given": {
"v": 1,
"id": "c3d4",
"kind": "check",
"language": "go",
"asker": "mesh-delivery-checker",
"at": "2026-11-05T10:00:00Z",
"check": {
"fetch": {
"repository": "novox/mesh-catalog",
"commit": "0123456789abcdef0123456789abcdef01234567",
"ref": "refs/pull/7/head",
"base": "89abcdef0123456789abcdef0123456789abcdef",
"beside": [
{
"repository": "novox/mesh-sdk",
"commit": "fedcba9876543210fedcba9876543210fedcba98"
}
],
"packages": {
"git.novox.be/novox/mesh-sdk/go": "fedcba9876543210fedcba9876543210fedcba98"
}
},
"levels": [
"unit",
"integration"
],
"kept": true
}
}
},
{
"name": "a finished build step with its artifacts",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "a1b2",
"tool": "build",
"node": "ace",
"state": "passed",
"started": "2026-11-05T10:00:00Z",
"ended": "2026-11-05T10:03:00Z",
"result": {
"artifacts": [
{
"artifact": "go",
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"reference": "container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"toolchain": {
"language": "go",
"version": "1.26.0",
"recipe": "1"
},
"imports": [
"git.novox.be/novox/mesh-sdk/go"
]
}
}
},
{
"name": "a finished check step with its levels",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "c3d4",
"tool": "check",
"node": "ace",
"state": "passed",
"started": "2026-11-05T10:00:00Z",
"ended": "2026-11-05T10:06:00Z",
"result": {
"fetch": {
"repository": "novox/mesh-catalog",
"commit": "0123456789abcdef0123456789abcdef01234567"
},
"levels": [
{
"level": "unit",
"digest": {
"level": "unit",
"packages": [
{
"package": "./modules/a",
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
]
},
"kept": [
{
"package": "./modules/a",
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"tests": 12
}
],
"tests": [
{
"package": "./modules/b",
"pass": true
}
],
"unmet": [
{
"package": "./modules/c",
"needs": [
"docker"
]
}
]
}
]
}
}
},
{
"name": "a declined step",
"of": "state:steps/<node>.<id>",
"given": {
"v": 1,
"id": "c3d5",
"tool": "check",
"node": "ace",
"state": "refused",
"started": "2026-11-05T10:00:00Z",
"result": {
"declined": true,
"needs": [
"docker"
],
"lacks": [
"docker"
]
}
}
}
]
+12 -10
View File
@@ -102,20 +102,22 @@ export function parseToolchainsByNodeLanguageKey(key: string): { node: string; l
return { node, language };
}
export type AskArtifact = buildStep.AskArtifact;
export type BuildAnswer = buildStep.BuildAnswer;
export type BuildAnswerArtifactsItem = buildStep.BuildAnswerArtifactsItem;
export type BuildAnswerToolchain = buildStep.BuildAnswerToolchain;
export type BuiltArtifact = buildStep.BuiltArtifact;
export type Capacity = buildStep.Capacity;
export type CheckAnswer = buildStep.CheckAnswer;
export type CheckAnswerLevelsItem = buildStep.CheckAnswerLevelsItem;
export type CheckAnswerLevelsItemDigest = buildStep.CheckAnswerLevelsItemDigest;
export type CheckAnswerLevelsItemDigestNeedsItem = buildStep.CheckAnswerLevelsItemDigestNeedsItem;
export type CheckAnswerLevelsItemDigestPackagesItem = buildStep.CheckAnswerLevelsItemDigestPackagesItem;
export type CheckAnswerLevelsItemKeptItem = buildStep.CheckAnswerLevelsItemKeptItem;
export type CheckAnswerLevelsItemUnmetItem = buildStep.CheckAnswerLevelsItemUnmetItem;
export type ComposeAnswer = buildStep.ComposeAnswer;
export type CurrentAnswer = buildStep.CurrentAnswer;
export type CurrentInput = buildStep.CurrentInput;
export type Fetch = buildStep.Fetch;
export type FetchBesideItem = buildStep.FetchBesideItem;
export type LevelAnswer = buildStep.LevelAnswer;
export type LevelAnswerDigest = buildStep.LevelAnswerDigest;
export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem;
export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem;
export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem;
export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem;
export type Pause = buildStep.Pause;
export type PauseAnswer = buildStep.PauseAnswer;
export type PauseInput = buildStep.PauseInput;
@@ -124,11 +126,11 @@ export type ResumeInput = buildStep.ResumeInput;
export type Step = buildStep.Step;
export type StepAsk = buildStep.StepAsk;
export type StepAskBuild = buildStep.StepAskBuild;
export type StepAskBuildArtifactsItem = buildStep.StepAskBuildArtifactsItem;
export type StepAskBuildToolchain = buildStep.StepAskBuildToolchain;
export type StepAskCheck = buildStep.StepAskCheck;
export type StepAskCompose = buildStep.StepAskCompose;
export type StepResult = buildStep.StepResult;
export type Toolchain = buildStep.Toolchain;
export type ToolchainUsed = buildStep.ToolchainUsed;
/** is the body of delivery. */
export type DeliveryAccept = StepAsk;
@@ -8,23 +8,26 @@ export const verbs = {
resume: "resume",
} as const;
export interface BuildAnswerArtifactsItem {
/** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */
export interface AskArtifact {
"artifact": string;
"digest": string;
"reference"?: string;
}
export interface BuildAnswerToolchain {
"language": string;
"recipe": string;
"version": string;
"from"?: string;
"seat"?: string;
"version"?: string;
}
export interface BuildAnswer {
"artifacts": BuildAnswerArtifactsItem[];
"artifacts": BuiltArtifact[];
"imports"?: string[];
"out-of-step"?: string[];
"toolchain"?: BuildAnswerToolchain;
"toolchain"?: ToolchainUsed;
}
/** one artifact a build made: its digest and the reference it was published under */
export interface BuiltArtifact {
"artifact": string;
"digest": string;
"reference"?: string;
}
/** how many steps the node runs at once */
@@ -47,49 +50,9 @@ export interface Capacity {
"why": string;
}
export interface CheckAnswerLevelsItemDigestNeedsItem {
"needs"?: string[];
"package"?: string;
}
export interface CheckAnswerLevelsItemDigestPackagesItem {
"digest": string;
"package": string;
}
export interface CheckAnswerLevelsItemDigest {
"beside"?: string[];
"judged_by"?: string;
"level"?: string;
"needs"?: CheckAnswerLevelsItemDigestNeedsItem[];
"packages"?: CheckAnswerLevelsItemDigestPackagesItem[];
"without_tests"?: string[];
}
export interface CheckAnswerLevelsItemKeptItem {
"digest"?: string;
"package"?: string;
"tests"?: number;
}
export interface CheckAnswerLevelsItemUnmetItem {
"needs"?: string[];
"package"?: string;
}
export interface CheckAnswerLevelsItem {
"digest"?: CheckAnswerLevelsItemDigest;
"judged"?: string;
"kept"?: CheckAnswerLevelsItemKeptItem[];
"level": string;
"tests"?: unknown[];
"unmet"?: CheckAnswerLevelsItemUnmetItem[];
}
export interface CheckAnswer {
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch": unknown;
"levels"?: CheckAnswerLevelsItem[];
"fetch": Fetch;
"levels"?: LevelAnswer[];
}
export interface ComposeAnswer {
@@ -99,6 +62,71 @@ export interface ComposeAnswer {
"verdict"?: unknown;
}
export interface FetchBesideItem {
"commit": string;
"repository": string;
}
/** what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside */
export interface Fetch {
/** the trunk commit a merge preview merges onto */
"base"?: string;
/** the sibling repositories placed beside the fetched tree */
"beside"?: FetchBesideItem[];
/** the full commit */
"commit": string;
/** the module's directory in the repository ("." for its root), with paths */
"dir"?: string;
/** the version (a full commit) of each mesh package to build against, by its module path */
"packages"?: Record<string, string>;
/** the paths the module names, relative to dir */
"paths"?: string[];
/** the pull request's ref, when only its head carries the commit */
"ref"?: string;
/** <owner>/<name> */
"repository": string;
}
export interface LevelAnswerDigestNeedsItem {
"needs"?: string[];
"package"?: string;
}
export interface LevelAnswerDigestPackagesItem {
"digest": string;
"package": string;
}
export interface LevelAnswerDigest {
"beside"?: string[];
"judged_by"?: string;
"level"?: string;
"needs"?: LevelAnswerDigestNeedsItem[];
"packages"?: LevelAnswerDigestPackagesItem[];
"without_tests"?: string[];
}
export interface LevelAnswerKeptItem {
"digest"?: string;
"package"?: string;
"tests"?: number;
}
export interface LevelAnswerUnmetItem {
"needs"?: string[];
"package"?: string;
}
/** one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test */
export interface LevelAnswer {
"digest"?: LevelAnswerDigest;
"judged"?: string;
"kept"?: LevelAnswerKeptItem[];
"level": string;
"tests"?: unknown[];
"unmet"?: LevelAnswerUnmetItem[];
}
/** whether the node takes no new ask */
export interface Pause {
/** when (RFC 3339 time) */
@@ -120,8 +148,7 @@ export interface Step {
/** when (RFC 3339 time) */
"phase-at"?: string;
"reason"?: string;
/** what the step answered */
"result"?: unknown;
"result"?: StepResult;
/** when (RFC 3339 time) */
"started": string;
/** running, passed, failed or refused */
@@ -132,33 +159,18 @@ export interface Step {
"what"?: string;
}
export interface StepAskBuildArtifactsItem {
"artifact": string;
"from"?: string;
"seat"?: string;
"version"?: string;
}
export interface StepAskBuildToolchain {
"language": string;
"recipe": string;
"version": string;
}
export interface StepAskBuild {
"artifacts": StepAskBuildArtifactsItem[];
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch"?: unknown;
"artifacts": AskArtifact[];
"fetch"?: Fetch;
"fp"?: string;
"module": string;
"path"?: string;
"publish"?: boolean;
"toolchain"?: StepAskBuildToolchain;
"toolchain"?: ToolchainUsed;
}
export interface StepAskCheck {
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch": unknown;
"fetch": Fetch;
"kept"?: boolean;
"levels": string[];
"packages"?: string[];
@@ -169,8 +181,7 @@ export interface StepAskCheck {
export interface StepAskCompose {
"dir": string;
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch": unknown;
"fetch": Fetch;
"package": string;
"reader"?: string;
/** the compose-candidate request */
@@ -197,6 +208,26 @@ export interface StepAsk {
"v"?: number;
}
/** what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four */
export interface StepResult {
"artifacts"?: BuiltArtifact[];
"built"?: boolean;
/** the holder declined the ask: its node lacks what the ask needs */
"declined"?: boolean;
"fetch"?: Fetch;
"imports"?: string[];
"key"?: string;
/** what the holder's node lacks of it */
"lacks"?: string[];
"levels"?: LevelAnswer[];
/** what the declined ask needs */
"needs"?: string[];
"out-of-step"?: string[];
"toolchain"?: ToolchainUsed;
/** the compose-candidate verdict (the composer's own layout) */
"verdict"?: unknown;
}
/** the toolchain a node reports */
export interface Toolchain {
/** when (RFC 3339 time) */
@@ -209,6 +240,13 @@ export interface Toolchain {
"version": string;
}
/** the toolchain a build was made with: the language, the installed version and the build module's recipe */
export interface ToolchainUsed {
"language": string;
"recipe": string;
"version": string;
}
/** is what current takes. */
export interface CurrentInput {
}
+12 -10
View File
@@ -102,20 +102,22 @@ export function parseToolchainsByNodeLanguageKey(key: string): { node: string; l
return { node, language };
}
export type AskArtifact = buildStep.AskArtifact;
export type BuildAnswer = buildStep.BuildAnswer;
export type BuildAnswerArtifactsItem = buildStep.BuildAnswerArtifactsItem;
export type BuildAnswerToolchain = buildStep.BuildAnswerToolchain;
export type BuiltArtifact = buildStep.BuiltArtifact;
export type Capacity = buildStep.Capacity;
export type CheckAnswer = buildStep.CheckAnswer;
export type CheckAnswerLevelsItem = buildStep.CheckAnswerLevelsItem;
export type CheckAnswerLevelsItemDigest = buildStep.CheckAnswerLevelsItemDigest;
export type CheckAnswerLevelsItemDigestNeedsItem = buildStep.CheckAnswerLevelsItemDigestNeedsItem;
export type CheckAnswerLevelsItemDigestPackagesItem = buildStep.CheckAnswerLevelsItemDigestPackagesItem;
export type CheckAnswerLevelsItemKeptItem = buildStep.CheckAnswerLevelsItemKeptItem;
export type CheckAnswerLevelsItemUnmetItem = buildStep.CheckAnswerLevelsItemUnmetItem;
export type ComposeAnswer = buildStep.ComposeAnswer;
export type CurrentAnswer = buildStep.CurrentAnswer;
export type CurrentInput = buildStep.CurrentInput;
export type Fetch = buildStep.Fetch;
export type FetchBesideItem = buildStep.FetchBesideItem;
export type LevelAnswer = buildStep.LevelAnswer;
export type LevelAnswerDigest = buildStep.LevelAnswerDigest;
export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem;
export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem;
export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem;
export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem;
export type Pause = buildStep.Pause;
export type PauseAnswer = buildStep.PauseAnswer;
export type PauseInput = buildStep.PauseInput;
@@ -124,11 +126,11 @@ export type ResumeInput = buildStep.ResumeInput;
export type Step = buildStep.Step;
export type StepAsk = buildStep.StepAsk;
export type StepAskBuild = buildStep.StepAskBuild;
export type StepAskBuildArtifactsItem = buildStep.StepAskBuildArtifactsItem;
export type StepAskBuildToolchain = buildStep.StepAskBuildToolchain;
export type StepAskCheck = buildStep.StepAskCheck;
export type StepAskCompose = buildStep.StepAskCompose;
export type StepResult = buildStep.StepResult;
export type Toolchain = buildStep.Toolchain;
export type ToolchainUsed = buildStep.ToolchainUsed;
/** is the body of delivery. */
export type DeliveryAccept = StepAsk;
@@ -8,23 +8,26 @@ export const verbs = {
resume: "resume",
} as const;
export interface BuildAnswerArtifactsItem {
/** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */
export interface AskArtifact {
"artifact": string;
"digest": string;
"reference"?: string;
}
export interface BuildAnswerToolchain {
"language": string;
"recipe": string;
"version": string;
"from"?: string;
"seat"?: string;
"version"?: string;
}
export interface BuildAnswer {
"artifacts": BuildAnswerArtifactsItem[];
"artifacts": BuiltArtifact[];
"imports"?: string[];
"out-of-step"?: string[];
"toolchain"?: BuildAnswerToolchain;
"toolchain"?: ToolchainUsed;
}
/** one artifact a build made: its digest and the reference it was published under */
export interface BuiltArtifact {
"artifact": string;
"digest": string;
"reference"?: string;
}
/** how many steps the node runs at once */
@@ -47,49 +50,9 @@ export interface Capacity {
"why": string;
}
export interface CheckAnswerLevelsItemDigestNeedsItem {
"needs"?: string[];
"package"?: string;
}
export interface CheckAnswerLevelsItemDigestPackagesItem {
"digest": string;
"package": string;
}
export interface CheckAnswerLevelsItemDigest {
"beside"?: string[];
"judged_by"?: string;
"level"?: string;
"needs"?: CheckAnswerLevelsItemDigestNeedsItem[];
"packages"?: CheckAnswerLevelsItemDigestPackagesItem[];
"without_tests"?: string[];
}
export interface CheckAnswerLevelsItemKeptItem {
"digest"?: string;
"package"?: string;
"tests"?: number;
}
export interface CheckAnswerLevelsItemUnmetItem {
"needs"?: string[];
"package"?: string;
}
export interface CheckAnswerLevelsItem {
"digest"?: CheckAnswerLevelsItemDigest;
"judged"?: string;
"kept"?: CheckAnswerLevelsItemKeptItem[];
"level": string;
"tests"?: unknown[];
"unmet"?: CheckAnswerLevelsItemUnmetItem[];
}
export interface CheckAnswer {
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch": unknown;
"levels"?: CheckAnswerLevelsItem[];
"fetch": Fetch;
"levels"?: LevelAnswer[];
}
export interface ComposeAnswer {
@@ -99,6 +62,71 @@ export interface ComposeAnswer {
"verdict"?: unknown;
}
export interface FetchBesideItem {
"commit": string;
"repository": string;
}
/** what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside */
export interface Fetch {
/** the trunk commit a merge preview merges onto */
"base"?: string;
/** the sibling repositories placed beside the fetched tree */
"beside"?: FetchBesideItem[];
/** the full commit */
"commit": string;
/** the module's directory in the repository ("." for its root), with paths */
"dir"?: string;
/** the version (a full commit) of each mesh package to build against, by its module path */
"packages"?: Record<string, string>;
/** the paths the module names, relative to dir */
"paths"?: string[];
/** the pull request's ref, when only its head carries the commit */
"ref"?: string;
/** <owner>/<name> */
"repository": string;
}
export interface LevelAnswerDigestNeedsItem {
"needs"?: string[];
"package"?: string;
}
export interface LevelAnswerDigestPackagesItem {
"digest": string;
"package": string;
}
export interface LevelAnswerDigest {
"beside"?: string[];
"judged_by"?: string;
"level"?: string;
"needs"?: LevelAnswerDigestNeedsItem[];
"packages"?: LevelAnswerDigestPackagesItem[];
"without_tests"?: string[];
}
export interface LevelAnswerKeptItem {
"digest"?: string;
"package"?: string;
"tests"?: number;
}
export interface LevelAnswerUnmetItem {
"needs"?: string[];
"package"?: string;
}
/** one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test */
export interface LevelAnswer {
"digest"?: LevelAnswerDigest;
"judged"?: string;
"kept"?: LevelAnswerKeptItem[];
"level": string;
"tests"?: unknown[];
"unmet"?: LevelAnswerUnmetItem[];
}
/** whether the node takes no new ask */
export interface Pause {
/** when (RFC 3339 time) */
@@ -120,8 +148,7 @@ export interface Step {
/** when (RFC 3339 time) */
"phase-at"?: string;
"reason"?: string;
/** what the step answered */
"result"?: unknown;
"result"?: StepResult;
/** when (RFC 3339 time) */
"started": string;
/** running, passed, failed or refused */
@@ -132,33 +159,18 @@ export interface Step {
"what"?: string;
}
export interface StepAskBuildArtifactsItem {
"artifact": string;
"from"?: string;
"seat"?: string;
"version"?: string;
}
export interface StepAskBuildToolchain {
"language": string;
"recipe": string;
"version": string;
}
export interface StepAskBuild {
"artifacts": StepAskBuildArtifactsItem[];
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch"?: unknown;
"artifacts": AskArtifact[];
"fetch"?: Fetch;
"fp"?: string;
"module": string;
"path"?: string;
"publish"?: boolean;
"toolchain"?: StepAskBuildToolchain;
"toolchain"?: ToolchainUsed;
}
export interface StepAskCheck {
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch": unknown;
"fetch": Fetch;
"kept"?: boolean;
"levels": string[];
"packages"?: string[];
@@ -169,8 +181,7 @@ export interface StepAskCheck {
export interface StepAskCompose {
"dir": string;
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch": unknown;
"fetch": Fetch;
"package": string;
"reader"?: string;
/** the compose-candidate request */
@@ -197,6 +208,26 @@ export interface StepAsk {
"v"?: number;
}
/** what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four */
export interface StepResult {
"artifacts"?: BuiltArtifact[];
"built"?: boolean;
/** the holder declined the ask: its node lacks what the ask needs */
"declined"?: boolean;
"fetch"?: Fetch;
"imports"?: string[];
"key"?: string;
/** what the holder's node lacks of it */
"lacks"?: string[];
"levels"?: LevelAnswer[];
/** what the declined ask needs */
"needs"?: string[];
"out-of-step"?: string[];
"toolchain"?: ToolchainUsed;
/** the compose-candidate verdict (the composer's own layout) */
"verdict"?: unknown;
}
/** the toolchain a node reports */
export interface Toolchain {
/** when (RFC 3339 time) */
@@ -209,6 +240,13 @@ export interface Toolchain {
"version": string;
}
/** the toolchain a build was made with: the language, the installed version and the build module's recipe */
export interface ToolchainUsed {
"language": string;
"recipe": string;
"version": string;
}
/** is what current takes. */
export interface CurrentInput {
}
+12 -10
View File
@@ -102,20 +102,22 @@ export function parseToolchainsByNodeLanguageKey(key: string): { node: string; l
return { node, language };
}
export type AskArtifact = buildStep.AskArtifact;
export type BuildAnswer = buildStep.BuildAnswer;
export type BuildAnswerArtifactsItem = buildStep.BuildAnswerArtifactsItem;
export type BuildAnswerToolchain = buildStep.BuildAnswerToolchain;
export type BuiltArtifact = buildStep.BuiltArtifact;
export type Capacity = buildStep.Capacity;
export type CheckAnswer = buildStep.CheckAnswer;
export type CheckAnswerLevelsItem = buildStep.CheckAnswerLevelsItem;
export type CheckAnswerLevelsItemDigest = buildStep.CheckAnswerLevelsItemDigest;
export type CheckAnswerLevelsItemDigestNeedsItem = buildStep.CheckAnswerLevelsItemDigestNeedsItem;
export type CheckAnswerLevelsItemDigestPackagesItem = buildStep.CheckAnswerLevelsItemDigestPackagesItem;
export type CheckAnswerLevelsItemKeptItem = buildStep.CheckAnswerLevelsItemKeptItem;
export type CheckAnswerLevelsItemUnmetItem = buildStep.CheckAnswerLevelsItemUnmetItem;
export type ComposeAnswer = buildStep.ComposeAnswer;
export type CurrentAnswer = buildStep.CurrentAnswer;
export type CurrentInput = buildStep.CurrentInput;
export type Fetch = buildStep.Fetch;
export type FetchBesideItem = buildStep.FetchBesideItem;
export type LevelAnswer = buildStep.LevelAnswer;
export type LevelAnswerDigest = buildStep.LevelAnswerDigest;
export type LevelAnswerDigestNeedsItem = buildStep.LevelAnswerDigestNeedsItem;
export type LevelAnswerDigestPackagesItem = buildStep.LevelAnswerDigestPackagesItem;
export type LevelAnswerKeptItem = buildStep.LevelAnswerKeptItem;
export type LevelAnswerUnmetItem = buildStep.LevelAnswerUnmetItem;
export type Pause = buildStep.Pause;
export type PauseAnswer = buildStep.PauseAnswer;
export type PauseInput = buildStep.PauseInput;
@@ -124,11 +126,11 @@ export type ResumeInput = buildStep.ResumeInput;
export type Step = buildStep.Step;
export type StepAsk = buildStep.StepAsk;
export type StepAskBuild = buildStep.StepAskBuild;
export type StepAskBuildArtifactsItem = buildStep.StepAskBuildArtifactsItem;
export type StepAskBuildToolchain = buildStep.StepAskBuildToolchain;
export type StepAskCheck = buildStep.StepAskCheck;
export type StepAskCompose = buildStep.StepAskCompose;
export type StepResult = buildStep.StepResult;
export type Toolchain = buildStep.Toolchain;
export type ToolchainUsed = buildStep.ToolchainUsed;
/** is the body of delivery. */
export type DeliveryAccept = StepAsk;
@@ -8,23 +8,26 @@ export const verbs = {
resume: "resume",
} as const;
export interface BuildAnswerArtifactsItem {
/** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */
export interface AskArtifact {
"artifact": string;
"digest": string;
"reference"?: string;
}
export interface BuildAnswerToolchain {
"language": string;
"recipe": string;
"version": string;
"from"?: string;
"seat"?: string;
"version"?: string;
}
export interface BuildAnswer {
"artifacts": BuildAnswerArtifactsItem[];
"artifacts": BuiltArtifact[];
"imports"?: string[];
"out-of-step"?: string[];
"toolchain"?: BuildAnswerToolchain;
"toolchain"?: ToolchainUsed;
}
/** one artifact a build made: its digest and the reference it was published under */
export interface BuiltArtifact {
"artifact": string;
"digest": string;
"reference"?: string;
}
/** how many steps the node runs at once */
@@ -47,49 +50,9 @@ export interface Capacity {
"why": string;
}
export interface CheckAnswerLevelsItemDigestNeedsItem {
"needs"?: string[];
"package"?: string;
}
export interface CheckAnswerLevelsItemDigestPackagesItem {
"digest": string;
"package": string;
}
export interface CheckAnswerLevelsItemDigest {
"beside"?: string[];
"judged_by"?: string;
"level"?: string;
"needs"?: CheckAnswerLevelsItemDigestNeedsItem[];
"packages"?: CheckAnswerLevelsItemDigestPackagesItem[];
"without_tests"?: string[];
}
export interface CheckAnswerLevelsItemKeptItem {
"digest"?: string;
"package"?: string;
"tests"?: number;
}
export interface CheckAnswerLevelsItemUnmetItem {
"needs"?: string[];
"package"?: string;
}
export interface CheckAnswerLevelsItem {
"digest"?: CheckAnswerLevelsItemDigest;
"judged"?: string;
"kept"?: CheckAnswerLevelsItemKeptItem[];
"level": string;
"tests"?: unknown[];
"unmet"?: CheckAnswerLevelsItemUnmetItem[];
}
export interface CheckAnswer {
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch": unknown;
"levels"?: CheckAnswerLevelsItem[];
"fetch": Fetch;
"levels"?: LevelAnswer[];
}
export interface ComposeAnswer {
@@ -99,6 +62,71 @@ export interface ComposeAnswer {
"verdict"?: unknown;
}
export interface FetchBesideItem {
"commit": string;
"repository": string;
}
/** what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside */
export interface Fetch {
/** the trunk commit a merge preview merges onto */
"base"?: string;
/** the sibling repositories placed beside the fetched tree */
"beside"?: FetchBesideItem[];
/** the full commit */
"commit": string;
/** the module's directory in the repository ("." for its root), with paths */
"dir"?: string;
/** the version (a full commit) of each mesh package to build against, by its module path */
"packages"?: Record<string, string>;
/** the paths the module names, relative to dir */
"paths"?: string[];
/** the pull request's ref, when only its head carries the commit */
"ref"?: string;
/** <owner>/<name> */
"repository": string;
}
export interface LevelAnswerDigestNeedsItem {
"needs"?: string[];
"package"?: string;
}
export interface LevelAnswerDigestPackagesItem {
"digest": string;
"package": string;
}
export interface LevelAnswerDigest {
"beside"?: string[];
"judged_by"?: string;
"level"?: string;
"needs"?: LevelAnswerDigestNeedsItem[];
"packages"?: LevelAnswerDigestPackagesItem[];
"without_tests"?: string[];
}
export interface LevelAnswerKeptItem {
"digest"?: string;
"package"?: string;
"tests"?: number;
}
export interface LevelAnswerUnmetItem {
"needs"?: string[];
"package"?: string;
}
/** one level of a check ask: the digest's answer, the kept verdicts taken, one test result per test step run, and the packages the node could not test */
export interface LevelAnswer {
"digest"?: LevelAnswerDigest;
"judged"?: string;
"kept"?: LevelAnswerKeptItem[];
"level": string;
"tests"?: unknown[];
"unmet"?: LevelAnswerUnmetItem[];
}
/** whether the node takes no new ask */
export interface Pause {
/** when (RFC 3339 time) */
@@ -120,8 +148,7 @@ export interface Step {
/** when (RFC 3339 time) */
"phase-at"?: string;
"reason"?: string;
/** what the step answered */
"result"?: unknown;
"result"?: StepResult;
/** when (RFC 3339 time) */
"started": string;
/** running, passed, failed or refused */
@@ -132,33 +159,18 @@ export interface Step {
"what"?: string;
}
export interface StepAskBuildArtifactsItem {
"artifact": string;
"from"?: string;
"seat"?: string;
"version"?: string;
}
export interface StepAskBuildToolchain {
"language": string;
"recipe": string;
"version": string;
}
export interface StepAskBuild {
"artifacts": StepAskBuildArtifactsItem[];
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch"?: unknown;
"artifacts": AskArtifact[];
"fetch"?: Fetch;
"fp"?: string;
"module": string;
"path"?: string;
"publish"?: boolean;
"toolchain"?: StepAskBuildToolchain;
"toolchain"?: ToolchainUsed;
}
export interface StepAskCheck {
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch": unknown;
"fetch": Fetch;
"kept"?: boolean;
"levels": string[];
"packages"?: string[];
@@ -169,8 +181,7 @@ export interface StepAskCheck {
export interface StepAskCompose {
"dir": string;
/** what the build module fetches: the repository, commit and directory, as the indexer states it */
"fetch": unknown;
"fetch": Fetch;
"package": string;
"reader"?: string;
/** the compose-candidate request */
@@ -197,6 +208,26 @@ export interface StepAsk {
"v"?: number;
}
/** what a step answered, by the kind of ask: a build's artifacts, toolchain, imports and out-of-step modules; a check's fetch and levels; a compose's built, key and verdict; a declined ask's declined, needs and lacks. The fields of the kinds do not meet, so one layout holds the four */
export interface StepResult {
"artifacts"?: BuiltArtifact[];
"built"?: boolean;
/** the holder declined the ask: its node lacks what the ask needs */
"declined"?: boolean;
"fetch"?: Fetch;
"imports"?: string[];
"key"?: string;
/** what the holder's node lacks of it */
"lacks"?: string[];
"levels"?: LevelAnswer[];
/** what the declined ask needs */
"needs"?: string[];
"out-of-step"?: string[];
"toolchain"?: ToolchainUsed;
/** the compose-candidate verdict (the composer's own layout) */
"verdict"?: unknown;
}
/** the toolchain a node reports */
export interface Toolchain {
/** when (RFC 3339 time) */
@@ -209,6 +240,13 @@ export interface Toolchain {
"version": string;
}
/** the toolchain a build was made with: the language, the installed version and the build module's recipe */
export interface ToolchainUsed {
"language": string;
"recipe": string;
"version": string;
}
/** is what current takes. */
export interface CurrentInput {
}
+257
View File
@@ -0,0 +1,257 @@
// Package asks holds the layout of the assigner's state `asks`: the keys of the asks the assigner puts to a person and the
// value it keeps at each, which the assigner writes and the view and the teller read (novox/hq ADR 0634 §1, ADR 0635 §5). It
// is the assigner's own published package. The gate's merge ask (`merge.<group>.<heads hash>`) needs proof of who answered
// (ADR 0421 §1), so it is answered on the phone alone until issue 953 closes. The assigner's asks need no such proof, and the
// view may answer them: the hold of a fingerprint whose health check failed (`hold.<module>.<fingerprint hex>`: release it or
// keep it held), the restore of a data module set back (`restore.<node>.<module>.<fingerprint hex>`: restore and lose newer
// writes, or leave it stopped, ADR 0429 §6), a backup that failed before an apply, a move a person must allow for a module of
// policy record, and the authorising ask of a proposal of placement.
package asks
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/sdk/kept"
"git.novox.be/novox/mesh-sdk/go/keys"
)
const (
// AskKindHold is the kind of the assigner's ask about a held fingerprint.
AskKindHold = "hold"
// AskKindRestore is the kind of the assigner's ask about a data module set back.
AskKindRestore = "restore"
// AskKindBackup is the kind of the assigner's ask about a backup before an apply that failed: back up again, or go on
// without one (novox/hq ADR 0539 clause 16).
AskKindBackup = "backup"
// AskKindRecord is the kind of the assigner's ask about a move a person must allow, for a module of policy record: a
// composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14).
AskKindRecord = "record"
// AskKindPlacement is the kind of the authorising ask about a proposal of placement (novox/hq ADR 0567 §5):
// `placement.<proposal id>`. The kind needs no proof, as the bus's put does not: its yes is an Approve, which only a
// channel proving who answered takes and the view never does (ViewAnswerFits).
AskKindPlacement = "placement"
)
// AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's Module
// and fingerprint FP, a restore's and a backup's Node, Module and FP, a record's Node, Module and the digest Composed of the
// composition it would allow, a placement's Proposal.
type AskKey struct {
Proposal string
Kind string
Group string
Hash string
Node string
Module string
FP string
Composed string
}
// AskNeedsProof says whether an ask of this kind must prove who answered it, so the view may not answer it: the merge's
// Approve does (ADR 0421 §1, issue 953), the assigner's hold and restore do not. An unknown kind is refused.
func AskNeedsProof(kind string) (bool, error) {
switch kind {
case meshdeliveryclient.AskKindMerge:
return true, nil
case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindPlacement:
return false, nil
}
return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold,
AskKindRestore, AskKindBackup, AskKindRecord, AskKindPlacement)
}
// HoldAskKey is the key of the ask about a module's held fingerprint: `hold.<module>.<hex>`.
func HoldAskKey(module, fingerprint string) (string, error) {
key, err := builds.BuilderBuildKey(module, fingerprint)
if err != nil {
return "", err
}
return AskKindHold + "." + key, nil
}
// RestoreAskKey is the key of the ask about a data module set back on a node from a fingerprint:
// `restore.<node>.<module>.<hex>`.
func RestoreAskKey(node, module, fingerprint string) (string, error) {
if err := keys.CheckName("node", node); err != nil {
return "", err
}
key, err := builds.BuilderBuildKey(module, fingerprint)
if err != nil {
return "", err
}
return AskKindRestore + "." + node + "." + key, nil
}
// BackupAskKey is the key of the ask about a module's failed backup on a node before it applies a fingerprint:
// `backup.<node>.<module>.<hex>`.
func BackupAskKey(node, module, fingerprint string) (string, error) {
part, err := meshcatalogclient.KeyPart(fingerprint)
if err != nil {
return "", err
}
key, err := nodeengineclient.ComposedKey(node, module, part)
if err != nil {
return "", err
}
return AskKindBackup + "." + key, nil
}
// PlacementAskKey is the key of the authorising ask about a proposal of placement: `placement.<proposal id>` (ADR 0567 §5).
func PlacementAskKey(proposal string) (string, error) {
if err := meshmoduleassignerclient.CheckProposalID(proposal); err != nil {
return "", err
}
return AskKindPlacement + "." + proposal, nil
}
// RecordAskKey is the key of the ask about a move of a module on a node to the composition whose digest it names:
// `record.<node>.<module>.<digest hex>`, so a new revision at the same fingerprint is asked again (ADR 0539 clause 14).
func RecordAskKey(node, module, composed string) (string, error) {
have, err := nodeengineclient.HaveKey(node, module)
if err != nil {
return "", err
}
if err := kept.CheckDigest(composed, "a record ask's composition digest"); err != nil {
return "", err
}
return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil
}
// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record or placement ask); it refuses an unknown
// kind and a key whose parts do not fit its kind, naming the key.
func ParseAnyAskKey(key string) (AskKey, error) {
kind, rest, _ := strings.Cut(key, ".")
refused := func(err error) (AskKey, error) {
return AskKey{}, fmt.Errorf("asks: %q is not an ask key: %w", key, err)
}
switch kind {
case meshdeliveryclient.AskKindMerge:
_, group, hash, err := meshdeliveryclient.ParseAskKey(key)
if err != nil {
return AskKey{}, err
}
return AskKey{Kind: kind, Group: group, Hash: hash}, nil
case AskKindHold:
module, fp, err := builds.ParseBuilderBuildKey(rest)
if err != nil {
return refused(err)
}
return AskKey{Kind: kind, Module: module, FP: fp}, nil
case AskKindRestore:
node, build, ok := strings.Cut(rest, ".")
if !ok {
return refused(fmt.Errorf("a restore ask is restore.<node>.<module>.<fingerprint hex>"))
}
if err := keys.CheckName("node", node); err != nil {
return refused(err)
}
module, fp, err := builds.ParseBuilderBuildKey(build)
if err != nil {
return refused(err)
}
return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil
case AskKindBackup:
node, module, part, isNode, err := nodeengineclient.ParseComposedKey(rest)
if err != nil || isNode {
return refused(fmt.Errorf("a backup ask is backup.<node>.<module>.<fingerprint hex>"))
}
fp, err := meshcatalogclient.ParseKeyPart(part)
if err != nil {
return refused(err)
}
return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil
case AskKindPlacement:
if err := meshmoduleassignerclient.CheckProposalID(rest); err != nil {
return refused(err)
}
return AskKey{Kind: kind, Proposal: rest}, nil
case AskKindRecord:
parts := strings.Split(rest, ".")
if len(parts) != 3 {
return refused(fmt.Errorf("a record ask is record.<node>.<module>.<digest hex>"))
}
node, module, isNode, err := nodeengineclient.ParseHaveKey(parts[0] + "." + parts[1])
if err != nil || isNode {
return refused(fmt.Errorf("a record ask is record.<node>.<module>.<digest hex>"))
}
composed := "sha256:" + parts[2]
if err := kept.CheckDigest(composed, "a record ask's composition digest"); err != nil {
return refused(err)
}
return AskKey{Kind: kind, Node: node, Module: module, Composed: composed}, nil
}
_, err := AskNeedsProof(kind)
return refused(err)
}
// Ask is the value of a key in the assigner's state asks: the router's ask whole, as the assigner composes it, so the teller
// publishes it unchanged. Asking counts the times the assigner wrote the ask at this key, as the gate's ask does: 0 or 1 for
// the first, n for the n-th, and the ask's id is meshdeliveryclient.AskingID(key, Asking), so each asking has its own id and
// an answer to an earlier asking is never taken for a later one. The ask is the operator-channel seat's as its JSON: its id,
// its options and its digest are checked by the assigner and the teller through operator-channel's client (novox/hq ADR
// 0627 §4).
type Ask struct {
V int `json:"v"`
Ask json.RawMessage `json:"ask"`
Asking int `json:"asking,omitempty"`
}
func (a Ask) check(key string) error {
ask, err := ParseAnyAskKey(key)
if err != nil {
return err
}
if proof, _ := AskNeedsProof(ask.Kind); proof {
return fmt.Errorf("asks: the assigner asks no %s ask: it needs proof of who answered", ask.Kind)
}
if a.Asking < 0 {
return fmt.Errorf("asks: the ask's asking %d is negative", a.Asking)
}
var fields map[string]json.RawMessage
if len(bytes.TrimSpace(a.Ask)) == 0 || json.Unmarshal(a.Ask, &fields) != nil || fields == nil {
return fmt.Errorf("asks: the value carries no router's ask")
}
return nil
}
// EncodeAsk is a, the ask at key, as JSON at keys.Version; it refuses a key of a kind that needs proof or of no kind, a
// negative asking, and a value carrying no router's ask.
func EncodeAsk(key string, a Ask) ([]byte, error) {
if err := a.check(key); err != nil {
return nil, err
}
a.V = keys.Version
return json.Marshal(a)
}
// DecodeAsk reads the ask at key; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeAsk refuses.
func DecodeAsk(key string, data []byte) (Ask, error) {
var a Ask
if err := kept.Decode(data, &a); err != nil {
return Ask{}, err
}
if err := a.check(key); err != nil {
return Ask{}, err
}
return a, nil
}
// SameAsk says whether two asks are the same value: the same version, asking and router's ask, its JSON compared compacted.
func SameAsk(a, b Ask) bool {
return a.V == b.V && a.Asking == b.Asking && bytes.Equal(compacted(a.Ask), compacted(b.Ask))
}
func compacted(raw json.RawMessage) []byte {
var b bytes.Buffer
if json.Compact(&b, raw) != nil {
return raw
}
return b.Bytes()
}
@@ -0,0 +1,85 @@
package asks
import (
"errors"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The keys equal what the assigner has always written (the expected strings are the trunk's own output for the same input),
// and each reads back to its parts.
func TestTheAssignersAskKeysKeepTheirFormsAndTheValueItsRules(t *testing.T) {
fp := "src2:" + strings.Repeat("ab", 32)
hex := strings.Repeat("ab", 32)
digest := "sha256:" + strings.Repeat("cd", 32)
hold, _ := HoldAskKey("gitea", fp)
restore, _ := RestoreAskKey("n1", "gitea", fp)
backup, _ := BackupAskKey("n1", "novox.be", fp)
placement, _ := PlacementAskKey("p-20261028t183000-1a2b3c")
record, _ := RecordAskKey("n1", "gitea", digest)
for key, want := range map[string]struct {
got string
back AskKey
}{
"hold": {hold, AskKey{Kind: "hold", Module: "gitea", FP: fp}},
"restore": {restore, AskKey{Kind: "restore", Node: "n1", Module: "gitea", FP: fp}},
"backup": {backup, AskKey{Kind: "backup", Node: "n1", Module: "novox.be", FP: fp}},
"placement": {placement, AskKey{Kind: "placement", Proposal: "p-20261028t183000-1a2b3c"}},
"record": {record, AskKey{Kind: "record", Node: "n1", Module: "gitea", Composed: digest}},
} {
back, err := ParseAnyAskKey(want.got)
if err != nil || back != want.back {
t.Errorf("%s: ParseAnyAskKey(%q) = %+v, %v; want %+v", key, want.got, back, err, want.back)
}
}
for got, want := range map[string]string{hold: "hold.gitea." + hex, restore: "restore.n1.gitea." + hex, backup: "backup.n1.novox=2ebe." + hex,
placement: "placement.p-20261028t183000-1a2b3c", record: "record.n1.gitea." + strings.Repeat("cd", 32)} {
if got != want {
t.Errorf("key %q, want %q", got, want)
}
}
merge, err := ParseAnyAskKey("merge.fix/1=2e2.d106c7950d28")
if err != nil || merge.Kind != "merge" || merge.Group != "fix/1.2" || merge.Hash != "d106c7950d28" {
t.Errorf("a merge ask key: %+v, %v", merge, err)
}
for _, bad := range []string{"", "other.x", "hold.gitea", "hold.gitea.src2:" + hex, "restore.n1", "backup.n1.node", "backup.n1.gitea." + hex[:60],
"placement.AB", "record.n1.gitea." + hex[:10], "record.n1.node." + hex, "merge.g"} {
if _, err := ParseAnyAskKey(bad); err == nil {
t.Errorf("ParseAnyAskKey(%q) reads a wrong key", bad)
}
}
if proof, err := AskNeedsProof("merge"); err != nil || !proof {
t.Error("a merge ask needs proof")
}
if proof, err := AskNeedsProof("hold"); err != nil || proof {
t.Error("a hold ask needs no proof")
}
if _, err := AskNeedsProof("view"); err == nil {
t.Error("an unknown kind of ask is taken")
}
// The value: the router's ask whole; an ask of a kind that needs proof, a negative asking and no ask are refused.
good := Ask{Ask: []byte(`{"id":"x"}`), Asking: 2}
data, err := EncodeAsk(hold, good)
if err != nil {
t.Fatalf("EncodeAsk: %v", err)
}
back, err := DecodeAsk(hold, data)
if err != nil || !SameAsk(back, Ask{V: keys.Version, Ask: []byte(` {"id": "x"} `), Asking: 2}) {
t.Errorf("DecodeAsk = %+v, %v", back, err)
}
for name, c := range map[string]struct {
key string
a Ask
}{"a merge ask": {"merge.g.d106c7950d28", good}, "a negative asking": {hold, Ask{Ask: good.Ask, Asking: -1}}, "no ask": {hold, Ask{}},
"a null ask": {hold, Ask{Ask: []byte("null")}}, "an unknown kind": {"other.x", good}} {
if _, err := EncodeAsk(c.key, c.a); err == nil {
t.Errorf("EncodeAsk takes %s", name)
}
}
if _, err := DecodeAsk(hold, []byte(`{"v":2,"ask":{}}`)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("DecodeAsk of another version = %v", err)
}
}
@@ -0,0 +1,13 @@
module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0
git.novox.be/novox/mesh-delivery/sdk v0.0.0
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
)
@@ -158,6 +158,15 @@
"MESH_VIEW_CALLERS_SEAT_HOLDERS": "node-tools",
"MESH_VIEW_CALLERS_WANTS": "mesh-grants"
}
},
{
"name": "asks",
"kind": "package",
"language": "go",
"paths": [
"asks"
],
"series": "0.1"
}
]
}
@@ -11,12 +11,12 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient/reference"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/fingerprint"
"git.novox.be/novox/mesh-delivery/sdk/kept"
"git.novox.be/novox/mesh-delivery/sdk/reference"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -62,13 +62,13 @@ const (
)
// BuilderBuildKey is the key, in BuilderBuildsBucket, of a module's build at a fingerprint: `<module>.<hex>`, the hex
// being the fingerprint's key part (fingerprint.KeyPart), since a bus key may not hold ':'. A module holding a dot or a
// being the fingerprint's key part (meshcatalogclient.KeyPart), since a bus key may not hold ':'. A module holding a dot or a
// character a key refuses, and a fingerprint not of the new form, are refused, naming them.
func BuilderBuildKey(module, fp string) (string, error) {
if err := kept.CheckName("module", module); err != nil {
if err := keys.CheckName("module", module); err != nil {
return "", err
}
part, err := fingerprint.KeyPart(fp)
part, err := meshcatalogclient.KeyPart(fp)
if err != nil {
return "", err
}
@@ -81,10 +81,10 @@ func ParseBuilderBuildKey(key string) (module, fp string, err error) {
if !ok {
return "", "", fmt.Errorf("builds: %q is not a builder build key, <module>.<fingerprint hex>", key)
}
if err := kept.CheckName("module", module); err != nil {
if err := keys.CheckName("module", module); err != nil {
return "", "", fmt.Errorf("builds: %q is not a builder build key: %w", key, err)
}
if fp, err = fingerprint.ParseKeyPart(part); err != nil {
if fp, err = meshcatalogclient.ParseKeyPart(part); err != nil {
return "", "", fmt.Errorf("builds: %q is not a builder build key: %w", key, err)
}
return module, fp, nil
@@ -265,7 +265,7 @@ func (b BuilderBuild) check() error {
return fmt.Errorf("builds: the build state %q is not queued, building, done or failed", b.State)
}
if b.Manifest != "" {
if !kept.IsHex64(b.Manifest) {
if !keys.IsHex64(b.Manifest) {
return fmt.Errorf("builds: %q is not a manifest digest, 64 lowercase hex digits", b.Manifest)
}
}
@@ -355,7 +355,7 @@ func EncodeBuilderBuild(b BuilderBuild) ([]byte, error) {
return json.Marshal(b)
}
// DecodeBuilderBuild reads a builder build value; it refuses an unknown version (kept.ErrUnknownVersion) and what
// DecodeBuilderBuild reads a builder build value; it refuses an unknown version (keys.ErrUnknownVersion) and what
// EncodeBuilderBuild refuses.
func DecodeBuilderBuild(data []byte) (BuilderBuild, error) {
var b BuilderBuild
@@ -9,7 +9,6 @@ import (
"testing"
"git.novox.be/novox/mesh-delivery/sdk/buildrecord"
"git.novox.be/novox/mesh-delivery/sdk/kept"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -177,10 +176,10 @@ func TestBuilderBuildNamesItsToolchains(t *testing.T) {
}
}
// 1358/AC5: a value of another version, or none, is refused with kept.ErrUnknownVersion.
// 1358/AC5: a value of another version, or none, is refused with keys.ErrUnknownVersion.
func TestBuilderBuildVersion(t *testing.T) {
for _, in := range []string{`{"v":2,"state":"queued"}`, `{"state":"queued"}`} {
if _, err := DecodeBuilderBuild([]byte(in)); !errors.Is(err, kept.ErrUnknownVersion) {
if _, err := DecodeBuilderBuild([]byte(in)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("DecodeBuilderBuild(%s) = %v", in, err)
}
}
@@ -3,6 +3,8 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0
git.novox.be/novox/mesh-delivery/sdk v0.0.0
+1 -2
View File
@@ -6,7 +6,6 @@ import (
"sort"
"time"
"git.novox.be/novox/mesh-delivery/sdk/kept"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -36,7 +35,7 @@ const (
// BuildLogKey is the key of a module's entries in the builder's log: the module's name, refused when a log key could not
// hold it.
func BuildLogKey(module string) (string, error) {
if err := kept.CheckName("module", module); err != nil {
if err := keys.CheckName("module", module); err != nil {
return "", err
}
return module, nil
@@ -6,7 +6,6 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/sdk/kept"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -26,7 +25,7 @@ func TestATakenValueRoundTripsAndRefusesWhatLacksARepositoryOrAMerge(t *testing.
t.Errorf("taken without merge: %v", err)
}
for _, in := range []string{`{"v":2,"repository":"a/b","merge":"c"}`, `{"repository":"a/b","merge":"c"}`} {
if _, err := DecodeTaken([]byte(in)); !errors.Is(err, kept.ErrUnknownVersion) {
if _, err := DecodeTaken([]byte(in)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("decode of %s = %v, want ErrUnknownVersion", in, err)
}
}
@@ -0,0 +1,48 @@
package viewstate
import (
"fmt"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
)
const viewWord = "view"
// ViewAnswerKey is the key, in the state answers, of the view's answer to the ask at askKey: `<ask key>.view`. An ask that needs
// proof of who answered is refused with its reason: it is answered on the phone (issue 953).
func ViewAnswerKey(askKey string) (string, error) {
ask, err := asks.ParseAnyAskKey(askKey)
if err != nil {
return "", err
}
if proof, _ := asks.AskNeedsProof(ask.Kind); proof {
return "", fmt.Errorf("viewstate: the ask %s needs proof of who answered: answer it on the phone (issue 953)", askKey)
}
return askKey + "." + viewWord, nil
}
// ParseViewAnswerKey is the inverse of ViewAnswerKey: the ask's key. It refuses a key without `.view`, an ask key of a kind that
// needs proof, and a key of another state, naming it.
func ParseViewAnswerKey(key string) (askKey string, err error) {
askKey, ok := strings.CutSuffix(key, "."+viewWord)
if !ok {
return "", fmt.Errorf("viewstate: %q is not a view answer key, <ask key>.view", key)
}
if _, err := ViewAnswerKey(askKey); err != nil {
return "", fmt.Errorf("viewstate: %q is not a view answer key: %w", key, err)
}
return askKey, nil
}
// ViewAnswerFits says whether the view's answer a answers the ask at key as it stands now: it refuses an answer to another
// asking of the ask (a stale answer to an earlier writing of the same key), naming both. Whether the ask offers the option
// chosen, and at the level acknowledge only (an option that authorises is answered on a channel that proves who answered,
// never in the view, which proves nobody), the teller reads from the ask through operator-channel's client.
func ViewAnswerFits(a ViewAnswer, key string, ask asks.Ask) error {
if want := meshdeliveryclient.AskingID(key, ask.Asking); a.Asking != want {
return fmt.Errorf("viewstate: the view's answer is to the asking %s, and the ask stands at the asking %s", a.Asking, want)
}
return nil
}
@@ -3,6 +3,8 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks v0.0.0
git.novox.be/novox/mesh-delivery/sdk v0.0.0
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
)
@@ -168,7 +168,7 @@ func EncodeViewAnswer(a ViewAnswer) ([]byte, error) {
return json.Marshal(a)
}
// DecodeViewAnswer reads an answer; it refuses an unknown version (kept.ErrUnknownVersion) and what EncodeViewAnswer refuses.
// DecodeViewAnswer reads an answer; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeViewAnswer refuses.
func DecodeViewAnswer(data []byte) (ViewAnswer, error) {
var a ViewAnswer
if err := kept.Decode(data, &a); err != nil {
@@ -4,6 +4,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -72,6 +74,32 @@ func TestReachValuesDifferingInAnyPartAreNotTheSame(t *testing.T) {
}
func TestViewAnswerRoundTrip(t *testing.T) {
t.Run("the key and the asking", func(t *testing.T) {
fp := "src2:" + strings.Repeat("ab", 32)
hold, _ := asks.HoldAskKey("gitea", fp)
key, err := ViewAnswerKey(hold)
if err != nil || key != hold+".view" {
t.Fatalf("ViewAnswerKey = %q, %v", key, err)
}
if back, err := ParseViewAnswerKey(key); err != nil || back != hold {
t.Errorf("ParseViewAnswerKey = %q, %v", back, err)
}
for _, bad := range []string{hold, "merge.g.d106c7950d28.view", "other.x.view"} {
if _, err := ParseViewAnswerKey(bad); err == nil {
t.Errorf("ParseViewAnswerKey(%q) reads a wrong key", bad)
}
}
if _, err := ViewAnswerKey("merge.g.d106c7950d28"); err == nil || !strings.Contains(err.Error(), "proof") {
t.Errorf("a merge ask is answered by the view: %v", err)
}
first := ViewAnswer{Asking: meshdeliveryclient.AskingID(hold, 0), Option: "release"}
if err := ViewAnswerFits(first, hold, asks.Ask{}); err != nil {
t.Errorf("an answer to the first asking is refused: %v", err)
}
if err := ViewAnswerFits(first, hold, asks.Ask{Asking: 2}); err == nil {
t.Error("an answer to an earlier asking is taken for the later one")
}
})
a := ViewAnswer{Asking: "hold-mesh-mail-aaaa-0123456789ab", Option: "release", Words: "fixed by #12"}
data, err := EncodeViewAnswer(a)
if err != nil {
+5 -44
View File
@@ -1,59 +1,20 @@
// Package kept holds what the layouts of the Delivery domain's kept values share: the version check on reading one, the
// rule a name must keep to be a key part, and the form of a digest. It names no seat and no module (ADR 0634 §2). The
// version itself (keys.Version) and the escaping of a name (keys.EscapeName) are mesh-sdk's, since more than one domain uses
// them.
// Package kept holds what the layouts of the Delivery domain's kept values share: the reading of one at its version and the form
// of a digest. It names no seat and no module (ADR 0634 §2). The version (keys.Version, keys.CheckVersion), the rule a name
// must keep to be a key part (keys.CheckName), keys.IsHex64 and the escaping of a name (keys.EscapeName) are mesh-sdk's,
// since more than one domain uses them.
package kept
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// ErrUnknownVersion is what Decode returns for a value whose version is missing or not keys.Version. Test it with errors.Is.
var ErrUnknownVersion = errors.New("kept: unknown value version")
var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`)
// CheckName refuses a name that is empty or holds a dot, a wildcard, whitespace or a character a bus key refuses.
func CheckName(kind, name string) error {
if name == "" {
return fmt.Errorf("kept: the %s name is empty", kind)
}
if !nameOK.MatchString(name) {
return fmt.Errorf("kept: the %s name %q holds a dot, a wildcard, whitespace or a character a key refuses", kind, name)
}
return nil
}
var hex64 = regexp.MustCompile(`^[0-9a-f]{64}$`)
// IsHex64 says whether s is 64 lowercase hex digits.
func IsHex64(s string) bool { return hex64.MatchString(s) }
// CheckVersion refuses a value with no version or one this reader does not read.
func CheckVersion(data []byte) error {
var probe struct {
V *int `json:"v"`
}
if err := json.Unmarshal(data, &probe); err != nil {
return fmt.Errorf("kept: value is not JSON: %w", err)
}
if probe.V == nil {
return fmt.Errorf("%w: no version in the value, this reader reads %d", ErrUnknownVersion, keys.Version)
}
if *probe.V != keys.Version {
return fmt.Errorf("%w: the value is version %d, this reader reads %d", ErrUnknownVersion, *probe.V, keys.Version)
}
return nil
}
// Decode reads data into into after CheckVersion.
func Decode(data []byte, into any) error {
if err := CheckVersion(data); err != nil {
if err := keys.CheckVersion(data); err != nil {
return err
}
if err := json.Unmarshal(data, into); err != nil {
+2 -9
View File
@@ -2,6 +2,7 @@ package kept
import (
"errors"
"git.novox.be/novox/mesh-sdk/go/keys"
"strings"
"testing"
)
@@ -10,21 +11,13 @@ func TestAKeptValueIsReadOnlyAtItsVersionAndAKeyPartOnlyOfKeyCharacters(t *testi
var v struct{ V int }
for data, ok := range map[string]bool{`{"v":1}`: true, `{"v":2}`: false, `{}`: false} {
err := Decode([]byte(data), &v)
if (err == nil) != ok || (!ok && !errors.Is(err, ErrUnknownVersion)) {
if (err == nil) != ok || (!ok && !errors.Is(err, keys.ErrUnknownVersion)) {
t.Errorf("Decode(%s) = %v", data, err)
}
}
if Decode([]byte(`nope`), &v) == nil {
t.Error("a value that is no JSON is read")
}
for name, ok := range map[string]bool{"a-b_c/d=": true, "": false, "a.b": false, "a b": false, "a*": false} {
if (CheckName("module", name) == nil) != ok {
t.Errorf("CheckName(%q) = %v", name, CheckName("module", name))
}
}
if !IsHex64(strings.Repeat("a", 64)) || IsHex64(strings.Repeat("A", 64)) || IsHex64("a") {
t.Error("IsHex64 reads a wrong form")
}
if CheckDigest("sha256:"+strings.Repeat("0", 64), "x") != nil || CheckDigest("sha256:0", "x") == nil {
t.Error("CheckDigest reads a wrong form")
}