421 lines
14 KiB
Go
421 lines
14 KiB
Go
package delivery
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// The modules of the parts release 3 adds (novox/hq ADR 0437): their buckets are their own state, `<module>_<name>`.
|
|
const (
|
|
GateModule = "mesh-delivery-gate"
|
|
IndexerModule = "mesh-delivery-indexer"
|
|
ForgeBridgeModule = "mesh-delivery-forge-bridge"
|
|
TellerModule = "mesh-delivery-teller"
|
|
)
|
|
|
|
// The gate's source bucket and the indexer's candidates and modules buckets (ADR 0437 §5). The source bucket has
|
|
// one key, SourceMainKey: every repository's merged head and the group that moved it, written by the gate alone by
|
|
// compare-and-set; its revision is the mesh's source revision. The indexer writes the other two.
|
|
const (
|
|
SourceBucket = GateModule + "_source"
|
|
SourceMainKey = "main"
|
|
SourceHistory = 64
|
|
CandidatesBucket = IndexerModule + "_candidates"
|
|
// CandidatesHistory is the revisions the candidates bucket keeps per key.
|
|
CandidatesHistory = 10
|
|
ModulesBucket = IndexerModule + "_modules"
|
|
// ModulesHistory is the revisions the modules bucket keeps per key.
|
|
ModulesHistory = 20
|
|
)
|
|
|
|
// ForgeStream is the forge bridge's stream as its manifest declares it; the broker derives the real names.
|
|
const ForgeStream = "forge"
|
|
|
|
// The kinds of forge message, the last part of a forge event's name.
|
|
const (
|
|
ForgePull = "pull"
|
|
ForgeMerged = "merged"
|
|
ForgeRefused = "refused"
|
|
)
|
|
|
|
// The actions of a pull message.
|
|
const (
|
|
PullOpened = "opened"
|
|
PullUpdated = "updated"
|
|
PullClosed = "closed"
|
|
)
|
|
|
|
// SourcePrefix opens every source fingerprint.
|
|
const SourcePrefix = "src1:"
|
|
|
|
// CandidatesKey is the key, in CandidatesBucket, of a group: `<group>`, the branch name the group's pull requests
|
|
// share (a slash is allowed); a lone pull request is a group of one under its branch name.
|
|
func CandidatesKey(group string) (string, error) {
|
|
if err := checkName("group", group); err != nil {
|
|
return "", err
|
|
}
|
|
return group, nil
|
|
}
|
|
|
|
// ParseCandidatesKey is the inverse of CandidatesKey.
|
|
func ParseCandidatesKey(key string) (group string, err error) {
|
|
if group, err = CandidatesKey(key); err != nil {
|
|
return "", fmt.Errorf("delivery: %q is not a candidates key: %w", key, err)
|
|
}
|
|
return group, nil
|
|
}
|
|
|
|
// ModulesKey is the key, in ModulesBucket, of a module: `<module>`.
|
|
func ModulesKey(module string) (string, error) {
|
|
if err := checkName("module", module); err != nil {
|
|
return "", err
|
|
}
|
|
return module, nil
|
|
}
|
|
|
|
// ParseModulesKey is the inverse of ModulesKey.
|
|
func ParseModulesKey(key string) (module string, err error) {
|
|
if module, err = ModulesKey(key); err != nil {
|
|
return "", fmt.Errorf("delivery: %q is not a modules key: %w", key, err)
|
|
}
|
|
return module, nil
|
|
}
|
|
|
|
// checkRepository refuses a repository that is not `<owner>/<name>`: exactly one slash, each part a valid name.
|
|
func checkRepository(repository string) error {
|
|
owner, name, ok := strings.Cut(repository, "/")
|
|
if !ok || strings.Contains(name, "/") {
|
|
return fmt.Errorf("delivery: the repository %q is not <owner>/<name>, with exactly one slash", repository)
|
|
}
|
|
if err := checkName("repository owner", owner); err != nil {
|
|
return fmt.Errorf("delivery: the repository %q: %w", repository, err)
|
|
}
|
|
if err := checkName("repository name", name); err != nil {
|
|
return fmt.Errorf("delivery: the repository %q: %w", repository, err)
|
|
}
|
|
if strings.Contains(owner, "/") {
|
|
return fmt.Errorf("delivery: the repository %q is not <owner>/<name>, with exactly one slash", repository)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkForgeKind(kind string) error {
|
|
switch kind {
|
|
case ForgePull, ForgeMerged, ForgeRefused:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("delivery: the forge kind %q is not pull, merged or refused", kind)
|
|
}
|
|
|
|
// ForgeEvent is the name of a forge message of the given kind: `forge.<owner>.<name>.<kind>`.
|
|
func ForgeEvent(repository, kind string) (string, error) {
|
|
if err := checkRepository(repository); err != nil {
|
|
return "", err
|
|
}
|
|
if err := checkForgeKind(kind); err != nil {
|
|
return "", err
|
|
}
|
|
owner, name, _ := strings.Cut(repository, "/")
|
|
return "forge." + owner + "." + name + "." + kind, nil
|
|
}
|
|
|
|
// ParseForgeEvent is the inverse of ForgeEvent.
|
|
func ParseForgeEvent(event string) (repository, kind string, err error) {
|
|
parts := strings.Split(event, ".")
|
|
if len(parts) != 4 || parts[0] != "forge" {
|
|
return "", "", fmt.Errorf("delivery: %q is not a forge event, forge.<owner>.<name>.<kind>", event)
|
|
}
|
|
repository = parts[1] + "/" + parts[2]
|
|
if _, err := ForgeEvent(repository, parts[3]); err != nil {
|
|
return "", "", fmt.Errorf("delivery: %q is not a forge event: %w", event, err)
|
|
}
|
|
return repository, parts[3], nil
|
|
}
|
|
|
|
// 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("delivery: 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
|
|
}
|
|
|
|
// Source is the value of SourceMainKey: every repository's merged head (repository to full sha, not only the
|
|
// group's) and the group that moved it.
|
|
type Source struct {
|
|
V int `json:"v"`
|
|
Group string `json:"group"`
|
|
Heads map[string]string `json:"heads"`
|
|
}
|
|
|
|
func (s Source) check() error {
|
|
if len(s.Heads) == 0 {
|
|
return fmt.Errorf("delivery: a source names no repository")
|
|
}
|
|
for repository, head := range s.Heads {
|
|
if repository == "" {
|
|
return fmt.Errorf("delivery: a source names a repository with no name")
|
|
}
|
|
if head == "" {
|
|
return fmt.Errorf("delivery: the source's head of %s is empty", repository)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Candidate is the value of a candidates key: the group's pull requests and heads, the heads hash, the base (the
|
|
// source revision it was previewed onto) and the modules each pull request touches. It carries no fingerprint
|
|
// until release 4 (ADR 0437 §5).
|
|
type Candidate struct {
|
|
V int `json:"v"`
|
|
Group string `json:"group"`
|
|
Base uint64 `json:"base"`
|
|
HeadsHash string `json:"heads"`
|
|
Pulls []CandidatePull `json:"pulls"`
|
|
}
|
|
|
|
// CandidatePull is one pull request of a candidate and the modules it touches.
|
|
type CandidatePull struct {
|
|
Head
|
|
Modules []string `json:"modules"`
|
|
}
|
|
|
|
func (c Candidate) check() error {
|
|
if len(c.Pulls) == 0 {
|
|
return fmt.Errorf("delivery: a candidate names no pull request")
|
|
}
|
|
heads := make([]Head, len(c.Pulls))
|
|
for i, p := range c.Pulls {
|
|
heads[i] = p.Head
|
|
}
|
|
want, _ := HeadsHash(heads)
|
|
if c.HeadsHash != want {
|
|
return fmt.Errorf("delivery: the candidate's heads hash %q is not the hash %q of its pull requests' heads", c.HeadsHash, want)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Module is the value of a modules key: the module's source fingerprint, or none and why (a build that resolves
|
|
// packages when built gets none, said here), and where it was computed: Repository and Commit are the trunk merge,
|
|
// Src the source revision then (0 before the gate writes source.main). Exactly one of FP and None is set.
|
|
type Module struct {
|
|
V int `json:"v"`
|
|
FP string `json:"fp,omitempty"`
|
|
None string `json:"none,omitempty"`
|
|
Src uint64 `json:"src,omitempty"`
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit"`
|
|
}
|
|
|
|
func (m Module) check() error {
|
|
switch {
|
|
case m.FP != "" && m.None != "":
|
|
return fmt.Errorf("delivery: a module has both a fingerprint and a reason for none")
|
|
case m.FP == "" && m.None == "":
|
|
return fmt.Errorf("delivery: a module has neither a fingerprint nor a reason for none")
|
|
case m.FP != "" && !strings.HasPrefix(m.FP, SourcePrefix):
|
|
return fmt.Errorf("delivery: the module's fingerprint %q does not start with %s", m.FP, SourcePrefix)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Forge is a message of the forge bridge's stream, trimmed to repository, branch, pull request and head. A pull
|
|
// message says Action; a merged message carries the merge commit, the tree of every directory holding a
|
|
// module.json and the repository's root tree; a refused message names the owed act it answers and the forge's own
|
|
// words. A pull message may also carry the module directories its head changes (Dirs, in the order the forge said) and
|
|
// DirsKnown, true when the forge could say them: known and empty is DirsKnown with no Dirs, not known is neither
|
|
// (the forge's module_dirs and module_dirs_said, mesh-catalog modules/gitea/trunk.ts). Other messages carry neither.
|
|
type Forge struct {
|
|
V int `json:"v"`
|
|
Kind string `json:"kind"`
|
|
Repository string `json:"repository"`
|
|
Branch string `json:"branch,omitempty"`
|
|
Number int `json:"number,omitempty"`
|
|
Head string `json:"head,omitempty"`
|
|
Action string `json:"action,omitempty"`
|
|
Dirs []string `json:"dirs,omitempty"`
|
|
DirsKnown bool `json:"dirs-known,omitempty"`
|
|
Merge string `json:"merge,omitempty"`
|
|
Trees map[string]string `json:"trees,omitempty"`
|
|
Root string `json:"root,omitempty"`
|
|
Act string `json:"act,omitempty"`
|
|
Words string `json:"words,omitempty"`
|
|
}
|
|
|
|
func (f Forge) check() error {
|
|
if err := checkRepository(f.Repository); err != nil {
|
|
return err
|
|
}
|
|
if err := checkForgeKind(f.Kind); err != nil {
|
|
return err
|
|
}
|
|
switch f.Kind {
|
|
case ForgePull:
|
|
if f.Number <= 0 {
|
|
return fmt.Errorf("delivery: a pull message needs its pull request number")
|
|
}
|
|
if f.Head == "" {
|
|
return fmt.Errorf("delivery: a pull message needs its head")
|
|
}
|
|
switch f.Action {
|
|
case PullOpened, PullUpdated, PullClosed:
|
|
default:
|
|
return fmt.Errorf("delivery: the pull action %q is not opened, updated or closed", f.Action)
|
|
}
|
|
if len(f.Dirs) > 0 && !f.DirsKnown {
|
|
return fmt.Errorf("delivery: a pull message with module directories must say it knows them (dirs-known)")
|
|
}
|
|
case ForgeMerged:
|
|
if len(f.Dirs) > 0 || f.DirsKnown {
|
|
return fmt.Errorf("delivery: a merged message carries no module directories")
|
|
}
|
|
if f.Number <= 0 {
|
|
return fmt.Errorf("delivery: a merged message needs its pull request number")
|
|
}
|
|
if f.Head == "" {
|
|
return fmt.Errorf("delivery: a merged message needs its head")
|
|
}
|
|
if f.Merge == "" {
|
|
return fmt.Errorf("delivery: a merged message needs its merge commit")
|
|
}
|
|
if f.Root == "" {
|
|
return fmt.Errorf("delivery: a merged message needs the repository's root tree")
|
|
}
|
|
if len(f.Trees) == 0 {
|
|
return fmt.Errorf("delivery: a merged message needs the tree of at least one directory holding a module.json")
|
|
}
|
|
for dir, tree := range f.Trees {
|
|
if tree == "" {
|
|
return fmt.Errorf("delivery: a merged message's tree of the directory %q is empty", dir)
|
|
}
|
|
}
|
|
case ForgeRefused:
|
|
if len(f.Dirs) > 0 || f.DirsKnown {
|
|
return fmt.Errorf("delivery: a refused message carries no module directories")
|
|
}
|
|
if f.Act == "" {
|
|
return fmt.Errorf("delivery: a refused message needs the act it answers")
|
|
}
|
|
if f.Words == "" {
|
|
return fmt.Errorf("delivery: a refused message needs the forge's words")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeSource is s as JSON at Version; it refuses a source with no repository or an empty head.
|
|
func EncodeSource(s Source) ([]byte, error) {
|
|
if err := s.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
s.V = Version
|
|
return json.Marshal(s)
|
|
}
|
|
|
|
// DecodeSource reads a source; it refuses an unknown version (ErrUnknownVersion) and what EncodeSource refuses.
|
|
func DecodeSource(data []byte) (Source, error) {
|
|
var s Source
|
|
if err := decode(data, &s); err != nil {
|
|
return Source{}, err
|
|
}
|
|
if err := s.check(); err != nil {
|
|
return Source{}, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// EncodeCandidate is c as JSON at Version; it refuses a heads hash that is not the hash of its pull requests' heads.
|
|
func EncodeCandidate(c Candidate) ([]byte, error) {
|
|
if err := c.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
c.V = Version
|
|
return json.Marshal(c)
|
|
}
|
|
|
|
// DecodeCandidate reads a candidate; it refuses an unknown version (ErrUnknownVersion) and what EncodeCandidate refuses.
|
|
func DecodeCandidate(data []byte) (Candidate, error) {
|
|
var c Candidate
|
|
if err := decode(data, &c); err != nil {
|
|
return Candidate{}, err
|
|
}
|
|
if err := c.check(); err != nil {
|
|
return Candidate{}, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// EncodeModule is m as JSON at Version; it refuses both or neither of a fingerprint and a reason, and a
|
|
// fingerprint not of the form src1:.
|
|
func EncodeModule(m Module) ([]byte, error) {
|
|
if err := m.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
m.V = Version
|
|
return json.Marshal(m)
|
|
}
|
|
|
|
// DecodeModule reads a module; it refuses an unknown version (ErrUnknownVersion) and what EncodeModule refuses.
|
|
func DecodeModule(data []byte) (Module, error) {
|
|
var m Module
|
|
if err := decode(data, &m); err != nil {
|
|
return Module{}, err
|
|
}
|
|
if err := m.check(); err != nil {
|
|
return Module{}, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// EncodeForge is f as JSON at Version; it refuses a message missing what its kind needs.
|
|
func EncodeForge(f Forge) ([]byte, error) {
|
|
if err := f.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
f.V = Version
|
|
return json.Marshal(f)
|
|
}
|
|
|
|
// DecodeForge reads a forge message; it refuses an unknown version (ErrUnknownVersion) and what EncodeForge refuses.
|
|
func DecodeForge(data []byte) (Forge, error) {
|
|
var f Forge
|
|
if err := decode(data, &f); err != nil {
|
|
return Forge{}, err
|
|
}
|
|
if err := f.check(); err != nil {
|
|
return Forge{}, err
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// SameSource says whether two sources are the same value, so a writer writes only when they differ.
|
|
func SameSource(a, b Source) bool { return reflect.DeepEqual(a, b) }
|
|
|
|
// SameCandidate says whether two candidates are the same value.
|
|
func SameCandidate(a, b Candidate) bool { return reflect.DeepEqual(a, b) }
|
|
|
|
// SameModule says whether two modules are the same value.
|
|
func SameModule(a, b Module) bool { return a == b }
|
|
|
|
// SameForge says whether two forge messages are the same value.
|
|
func SameForge(a, b Forge) bool { return reflect.DeepEqual(a, b) }
|