Build a change once: the check builds what its merge moves, the walk takes it by key
mesh/merge-gate out of date: judged against trunk bff5b7f0e4f8, the trunk is now 372faebf2824; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk bff5b7f0e4f8, the trunk is now 372faebf2824; judged again on a recheck or when it is merged
mesh/delivery-group group feat/350-one-build-per-change checking: composed check out of date: novox/mesh-controller@cd49b50c4075 was judged against trunk bff5b…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate out of date: judged against trunk bff5b7f0e4f8, the trunk is now 372faebf2824; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk bff5b7f0e4f8, the trunk is now 372faebf2824; judged again on a recheck or when it is merged
mesh/delivery-group group feat/350-one-build-per-change checking: composed check out of date: novox/mesh-controller@cd49b50c4075 was judged against trunk bff5b…
mesh/delivery superseded: a newer head of the same pull request
A merge built every moved module a second time although its check had judged the same tree (issue 649). The check now builds into the scratch namespace under a reuse key (source fingerprint plus the builder's own digest), and the walk of the trunk commit copies a build of the same key by digest instead of building, saying per module why when it cannot (novox/hq ADR 0350).
This commit is contained in:
+184
-3
@@ -91,6 +91,16 @@ type Result struct {
|
||||
// (novox/hq issue 280).
|
||||
Source string
|
||||
|
||||
// ReuseKey is the build's reuse key (novox/hq ADR 0350 §1): its source fingerprint's parts with the builder's
|
||||
// version, and ReuseParts those parts; empty, with NoKey saying why, where the source does not pin the build.
|
||||
ReuseKey string
|
||||
ReuseParts map[string]string
|
||||
NoKey string
|
||||
// Reused is the check build a walk took instead of building (ADR 0350 §2), and Why, when it built, why it did
|
||||
// not take one (§3). Both empty for a build not asked to reuse.
|
||||
Reused *Reused
|
||||
Why string
|
||||
|
||||
// Sources are what this build was made from, as files (novox/hq ADR 0267 rule 1): per repository, the
|
||||
// entries a changed file is tested against (SourceHolds). The module's own repository has an empty
|
||||
// Repository. Said only for a build of a commit on the trunk, and only for a repository whose every
|
||||
@@ -136,14 +146,41 @@ func Build(ctx context.Context, run Runner, publish Publisher,
|
||||
// one record that it is there.
|
||||
var mirrored []string
|
||||
result, err := build(ctx, run, publish, repository, path, ref, workspace, held, npmrc, forge, log,
|
||||
&mirrored, seats...)
|
||||
&mirrored, nil, seats...)
|
||||
result.Mirrored = mirrored
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Reuse is what a walk's build may take instead of building (novox/hq ADR 0350 §2): the scratch namespace's check
|
||||
// builds, and the pull requests the walk answers, whose checks name a differing key.
|
||||
type Reuse struct {
|
||||
Scratch Scratch
|
||||
Pulls []PullRef
|
||||
// keyFirst is a check's build (§1): its key is taken before anything is built, and a module with none is not
|
||||
// built at all — nothing could ever take it.
|
||||
keyFirst bool
|
||||
}
|
||||
|
||||
// errNoKey is a check's build of a module whose source does not pin it: not built (ADR 0350 §1).
|
||||
var errNoKey = errors.New("it has no reuse key")
|
||||
|
||||
// BuildReusing is Build for the walk of a trunk commit (novox/hq ADR 0350 §2): before building, it takes the reuse
|
||||
// key of the commit's tree, and where a check build holds the same key it copies that build into the module's
|
||||
// namespace by digest and answers with it, building nothing. Otherwise it builds, and Result.Why says why (§3).
|
||||
// Only a commit on its trunk takes a check build: anything else builds, as Build does.
|
||||
func BuildReusing(ctx context.Context, run Runner, publish Publisher, reuse Reuse,
|
||||
repository, path, ref, workspace string, held map[string]string, npmrc Npmrc,
|
||||
forge GitCredential, log Log, seats ...map[string]string) (Result, error) {
|
||||
var mirrored []string
|
||||
result, err := build(ctx, run, publish, repository, path, ref, workspace, held, npmrc, forge, log,
|
||||
&mirrored, &reuse, seats...)
|
||||
result.Mirrored = mirrored
|
||||
return result, err
|
||||
}
|
||||
|
||||
func build(ctx context.Context, run Runner, publish Publisher,
|
||||
repository, path, ref, workspace string, held map[string]string, npmrc Npmrc,
|
||||
forge GitCredential, log Log, mirrored *[]string, seats ...map[string]string) (Result, error) {
|
||||
forge GitCredential, log Log, mirrored *[]string, reuse *Reuse, seats ...map[string]string) (Result, error) {
|
||||
// The clone base of each seat a context may name (novox/hq ADR 0155); variadic so the callers
|
||||
// that hand none — tests of everything but contexts — read as they did.
|
||||
var seatBases map[string]string
|
||||
@@ -284,6 +321,9 @@ func build(ctx context.Context, run Runner, publish Publisher,
|
||||
var built []catalogue.Built
|
||||
// stoodOn is every base the build was handed, as resolved — the edges the catalogue derives.
|
||||
var stoodOn []string
|
||||
// reused is the check build a walk took, and builtWhy why it built instead (novox/hq ADR 0350).
|
||||
var reused *Reused
|
||||
builtWhy := ""
|
||||
if manifest.Build != nil {
|
||||
// What this module said it stands on, answered with what this mesh actually holds. Done
|
||||
// before anything is built, so a missing base is refused in front of the person who can
|
||||
@@ -327,7 +367,35 @@ func build(ctx context.Context, run Runner, publish Publisher,
|
||||
// Ordered, so two builds of one commit do the same work in the same sequence and their
|
||||
// logs can be compared.
|
||||
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
|
||||
// **A walk takes the check's build of the same key** (novox/hq ADR 0350 §2): only a commit on its trunk, and
|
||||
// only when asked — a check, a dry run, `build --ref` and `rebuild` never are. Its key is taken by the same
|
||||
// steps a build takes, stopped before anything is built; what it takes is copied by digest.
|
||||
if reuse != nil && reuse.keyFirst {
|
||||
_, key, err := keyFor(ctx, run, publish, manifest, within, workspace, commit, credentials, artifacts, args,
|
||||
held, npmrcPath, npmrc, seatBases, src, say)
|
||||
if err == nil && key.Key == "" {
|
||||
err = fmt.Errorf("%w: %s", errNoKey, key.Why)
|
||||
}
|
||||
if err != nil {
|
||||
say("reuse", "not built for the merge: %v", err)
|
||||
return Result{Commit: commit, NoKey: err.Error()}, err
|
||||
}
|
||||
} else if reuse != nil && reuse.Scratch != nil {
|
||||
taken, keyed, why := takeACheckBuild(ctx, run, publish, *reuse, manifest, within, workspace, commit,
|
||||
credentials, artifacts, args, held, npmrcPath, npmrc, seatBases, src, onTrunk, say)
|
||||
if taken != nil {
|
||||
built = taken.Made
|
||||
src = keyed
|
||||
reused = taken
|
||||
} else {
|
||||
builtWhy = why
|
||||
say("reuse", "built: %s", why)
|
||||
}
|
||||
}
|
||||
for _, a := range artifacts {
|
||||
if reused != nil {
|
||||
break
|
||||
}
|
||||
say("artifact", "%s (%s%s) — starting", a.Name, a.Kind, langSuffix(a))
|
||||
made, err := one(ctx, run, publish, manifest.Module, within, workspace, commit, credentials, a, args, held, npmrcPath, npmrc, seatBases, src, say)
|
||||
if err != nil {
|
||||
@@ -363,9 +431,102 @@ func build(ctx context.Context, run Runner, publish Publisher,
|
||||
say("source", "%d path(s) of %s", len(s.Paths), where)
|
||||
}
|
||||
}
|
||||
// The reuse key (novox/hq ADR 0350 §1): what a check keeps its build under, and what a walk takes one by.
|
||||
key := keyOf(src, manifest, Version)
|
||||
return Result{Manifest: resolved, Commit: commit, Built: built,
|
||||
Against: against(within, manifest, stoodOn), Read: readBy(manifest), Source: fingerprint,
|
||||
Trunk: trunk, OnTrunk: onTrunk, Branches: branches, Sources: sources}, nil
|
||||
Trunk: trunk, OnTrunk: onTrunk, Branches: branches, Sources: sources,
|
||||
ReuseKey: key.Key, ReuseParts: key.Parts, NoKey: key.Why, Reused: reused, Why: builtWhy}, nil
|
||||
}
|
||||
|
||||
// takeACheckBuild is the walk's look for a check build of its key (novox/hq ADR 0350 §2, §3): the key taken by
|
||||
// the steps a build takes, each artifact stopped before it is built; the build kept under it copied by digest into
|
||||
// the module's namespace. Answers what it took and the source inputs its key was taken from, or why it builds. A
|
||||
// copy that fails is said, and the walk builds (§3): it never fails the walk on its own.
|
||||
func takeACheckBuild(ctx context.Context, run Runner, publish Publisher, reuse Reuse, manifest catalogue.Manifest,
|
||||
within, workspace, commit, credentials string, artifacts []catalogue.Artifact, args []string,
|
||||
held map[string]string, npmrcPath string, npmrc Npmrc, seats map[string]string, src *sourceInputs, onTrunk bool,
|
||||
say func(step, format string, args ...any)) (*Reused, *sourceInputs, string) {
|
||||
if !onTrunk {
|
||||
return nil, nil, "the commit is not on its trunk, and only a trunk commit's walk takes a check's build"
|
||||
}
|
||||
keyed, key, err := keyFor(ctx, run, publish, manifest, within, workspace, commit, credentials, artifacts, args,
|
||||
held, npmrcPath, npmrc, seats, src, say)
|
||||
if err != nil {
|
||||
return nil, nil, err.Error()
|
||||
}
|
||||
if key.Key == "" {
|
||||
return nil, nil, "it has no reuse key: " + key.Why
|
||||
}
|
||||
found, ok, err := reuse.Scratch.Find(ctx, manifest.Module, key.Key)
|
||||
if err != nil {
|
||||
return nil, nil, "the scratch namespace could not be read: " + err.Error()
|
||||
}
|
||||
if !ok {
|
||||
return nil, nil, notKeptWhy(ctx, reuse, manifest.Module, key)
|
||||
}
|
||||
taken := &Reused{From: found}
|
||||
for _, made := range found.Made {
|
||||
copied, err := reuse.Scratch.Take(ctx, manifest.Module, made)
|
||||
if err != nil {
|
||||
why := fmt.Sprintf("copying %s's %s from %s failed (%v)", manifest.Module, made.Name, found.Said(), err)
|
||||
say("reuse", "%s; built instead", why)
|
||||
return nil, nil, why
|
||||
}
|
||||
taken.Made = append(taken.Made, copied)
|
||||
}
|
||||
// Every artifact the manifest declares, or none: a check build missing one is not this build.
|
||||
if len(taken.Made) != len(artifacts) {
|
||||
return nil, nil, fmt.Sprintf("%s holds %d artifact(s) of %s, which declares %d", found.Said(), len(taken.Made),
|
||||
manifest.Module, len(artifacts))
|
||||
}
|
||||
say("reuse", "%s", taken.Words())
|
||||
return taken, keyed, ""
|
||||
}
|
||||
|
||||
// keyFor takes a build's reuse key by the steps a build takes, each artifact stopped before anything is built or
|
||||
// published (ADR 0350 §2): its own copy of the source inputs, so the build's are untouched.
|
||||
func keyFor(ctx context.Context, run Runner, publish Publisher, manifest catalogue.Manifest,
|
||||
within, workspace, commit, credentials string, artifacts []catalogue.Artifact, args []string,
|
||||
held map[string]string, npmrcPath string, npmrc Npmrc, seats map[string]string, src *sourceInputs,
|
||||
say func(step, format string, args ...any)) (*sourceInputs, reuseKey, error) {
|
||||
keyed := src.copy()
|
||||
keyed.onlyKey = true
|
||||
for _, a := range artifacts {
|
||||
if _, err := one(ctx, run, publish, manifest.Module, within, workspace, commit, credentials, a, args, held,
|
||||
npmrcPath, npmrc, seats, keyed, say); err != nil {
|
||||
return nil, reuseKey{}, fmt.Errorf("its reuse key could not be taken: %w", err)
|
||||
}
|
||||
}
|
||||
keyed.onlyKey = false
|
||||
return keyed, keyOf(keyed, manifest, Version), nil
|
||||
}
|
||||
|
||||
// notKeptWhy is why no check build has the key (§3): no check of the walk's pull requests built the module, the
|
||||
// key differs — naming the part — or the build it made is gone.
|
||||
func notKeptWhy(ctx context.Context, reuse Reuse, module string, key reuseKey) string {
|
||||
if len(reuse.Pulls) == 0 {
|
||||
return "no check built it with this key"
|
||||
}
|
||||
builds, err := reuse.Scratch.OfPulls(ctx, reuse.Pulls)
|
||||
if err != nil {
|
||||
return "no check build has this key, and which checks built it could not be read: " + err.Error()
|
||||
}
|
||||
for _, b := range builds {
|
||||
if b.Module != module {
|
||||
continue
|
||||
}
|
||||
if b.Key == key.Key {
|
||||
return b.Said() + " built it with this key, and it was removed"
|
||||
}
|
||||
return "the key differs from " + b.Said() + "'s in " + strings.Join(differing(b.Parts, key.Parts), ", ")
|
||||
}
|
||||
var pulls []string
|
||||
for _, p := range reuse.Pulls {
|
||||
pulls = append(pulls, p.String())
|
||||
}
|
||||
return "no check of " + strings.Join(pulls, ", ") + " built it (a module its check did not move, or a check " +
|
||||
"that did not finish)"
|
||||
}
|
||||
|
||||
// branchesHolding is every branch of a fresh clone's origin the commit is on, without `origin/`.
|
||||
@@ -754,6 +915,10 @@ func one(ctx context.Context, run Runner, publish Publisher,
|
||||
|
||||
switch a.Kind {
|
||||
case catalogue.ArtifactUpstream:
|
||||
// Its key is the image it copies, read from the manifest (keyOf): nothing to take here.
|
||||
if src.keyOnly() {
|
||||
return catalogue.Built{Name: a.Name, Kind: a.Kind}, nil
|
||||
}
|
||||
// Mirrored, not built: copied under a name of the mesh's own, so what a machine fetches is
|
||||
// pinned by a digest this registry assigned rather than by a tag somebody else can move.
|
||||
//
|
||||
@@ -882,6 +1047,10 @@ func one(ctx context.Context, run Runner, publish Publisher,
|
||||
} else {
|
||||
src.ownWhole()
|
||||
}
|
||||
// Taken for a reuse key (novox/hq ADR 0350 §2): everything its fingerprint reads is read; nothing is built.
|
||||
if src.keyOnly() {
|
||||
return catalogue.Built{Name: a.Name, Kind: a.Kind}, nil
|
||||
}
|
||||
invocation := append([]string{"build", "-f", recipePath, "-t", local}, args...)
|
||||
if a.Target != "" {
|
||||
invocation = append(invocation, "--target", a.Target)
|
||||
@@ -949,6 +1118,9 @@ func one(ctx context.Context, run Runner, publish Publisher,
|
||||
src.notPinned(a.Name + " resolves packages of its own at build time")
|
||||
}
|
||||
}
|
||||
if src.keyOnly() {
|
||||
return catalogue.Built{Name: a.Name, Kind: a.Kind}, nil
|
||||
}
|
||||
// The module's own packages first, where the compiler and the bundler resolve them from
|
||||
// (dependencies.go); nothing at all for a module whose package.json names only the SDK.
|
||||
if err := installOwn(ctx, run, tree, chain, base, registry, say); err != nil {
|
||||
@@ -992,6 +1164,9 @@ func one(ctx context.Context, run Runner, publish Publisher,
|
||||
say("package", "building and publishing %s (%s)", a.Name, a.Language)
|
||||
src.ownWhole()
|
||||
src.notPinned(a.Name + " is a package, built from what the registry holds when it is built")
|
||||
if src.keyOnly() {
|
||||
return catalogue.Built{Name: a.Name, Kind: a.Kind}, nil
|
||||
}
|
||||
reference, err := publishPackage(ctx, run, module, tree, a, npmrc, say)
|
||||
if err != nil {
|
||||
return catalogue.Built{}, fmt.Errorf("%s: publishing %s failed: %w", module, a.Name, err)
|
||||
@@ -1000,6 +1175,12 @@ func one(ctx context.Context, run Runner, publish Publisher,
|
||||
return catalogue.Built{Name: a.Name, Kind: a.Kind, Reference: reference}, nil
|
||||
|
||||
case catalogue.ArtifactArchive:
|
||||
if src.keyOnly() {
|
||||
if a.URL == "" {
|
||||
src.ownHas(cleanEntry(a.From) + "/**")
|
||||
}
|
||||
return catalogue.Built{Name: a.Name, Kind: a.Kind}, nil
|
||||
}
|
||||
var body []byte
|
||||
var err error
|
||||
if a.URL != "" {
|
||||
|
||||
Reference in New Issue
Block a user