Files
mesh-controller/internal/inventory/builds.go
T
jschoubben 0bbb5c6838 Builds have a history, and failures are rows like any other
A build result was answered to whoever asked and kept nowhere. So "when
did this last build", "why did it fail" and "which machine built what is
running" had no answer, and a build nobody was waiting for was reported
into the void — which is the same as not reporting it.

Failures are recorded too, and that is the point rather than a detail: a
failed build that leaves no trace is indistinguishable from one nobody
asked for, and the difference is the whole of whether somebody should be
looking at something. A build that never learned what it was building
keeps the repository, because that is what a person goes and looks at.

Recording is idempotent on the correlation id, because a result can
arrive twice — as the answer to whoever asked, and on the exchange when
nobody was. Two rows would show one build as two, and which is real is
not answerable afterwards.

The serving control plane now binds `built` as well, so results from
builds it did not ask for are kept. It refuses them loudly when it has
nowhere to put them rather than dropping them, so the broker's own
counters show something arriving that nothing handles.

`builds [<module>]` reads it: what happened lately across the mesh, or
what has happened to one module — the first asked after something goes
wrong, the second when deciding whether to trust something.

What was published is kept with the build, so a digest traces back to
what made it without holding the manifest twice in a place that can
disagree with the first.
2026-08-30 10:18:23 +02:00

103 lines
3.1 KiB
Go

package inventory
import (
"context"
"encoding/json"
"time"
)
// What has been built.
//
// A build result was answered to whoever asked and kept nowhere, so "when did this last build",
// "why did it fail" and "which machine built what is running" had no answer. Failures are recorded
// too: one that leaves no trace is indistinguishable from a build nobody asked for, and the
// difference is the whole of whether somebody should be looking at something.
// Build is one attempt, whichever way it went.
type Build struct {
ID string
Repository string
Ref string
// Module is empty for a build that failed before it knew what it was building.
Module string
Commit string
// On is the machine that did it.
On string
// Failed is the builder's own words, empty when it worked.
Failed string
Made []Artifact
At time.Time
}
// Artifact is one thing a build published.
type Artifact struct {
Name string `json:"name"`
Kind string `json:"kind"`
Reference string `json:"reference"`
}
// Worked reports whether this build produced something.
func (b Build) Worked() bool { return b.Failed == "" }
// RecordBuild keeps what a builder said.
//
// Idempotent on the correlation id, because a result can arrive twice: once as the answer to
// whoever asked and once on the exchange when nobody was. Recording both would show one build as
// two, and which of the two is real is not a question anybody could answer afterwards.
func (i *Inventory) RecordBuild(ctx context.Context, b Build) error {
made, err := json.Marshal(b.Made)
if err != nil {
return err
}
var module *string
if b.Module != "" {
module = &b.Module
}
_, err = i.store.Pool().Exec(ctx,
`insert into build (id, repository, ref, module, commit_hash, built_on, failed, made)
values ($1, $2, $3, $4, $5, $6, $7, $8)
on conflict (id) do nothing`,
b.ID, b.Repository, b.Ref, module, b.Commit, b.On, b.Failed, made)
return err
}
// Builds is what has happened lately, newest first.
//
// For one module when named, or across the mesh when not. Both are asked: *what happened just
// now* after something goes wrong, and *what has happened to this* when deciding whether to
// trust it.
func (i *Inventory) Builds(ctx context.Context, module string, limit int) ([]Build, error) {
if limit <= 0 {
limit = 20
}
query := `select id, repository, ref, coalesce(module,''), commit_hash, built_on, failed, made, at
from build order by at desc limit $1`
args := []any{limit}
if module != "" {
query = `select id, repository, ref, coalesce(module,''), commit_hash, built_on, failed, made, at
from build where module = $2 order by at desc limit $1`
args = append(args, module)
}
rows, err := i.store.Pool().Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Build
for rows.Next() {
var b Build
var made []byte
if err := rows.Scan(&b.ID, &b.Repository, &b.Ref, &b.Module, &b.Commit,
&b.On, &b.Failed, &made, &b.At); err != nil {
return nil, err
}
if err := json.Unmarshal(made, &b.Made); err != nil {
return nil, err
}
out = append(out, b)
}
return out, rows.Err()
}