RN11: the runner says a failed head as the finding pipeline-failed
A required step that failed or could not run writes the finding of its commit, owned by the commit's author as the forge records it (read through the seat git's pull); one with no known author has no owner and says why, written at the next step end of that commit; a newer revision passing the steps clears it. The runner's finding writer is the held writer. Builds: design 47 G §33, §540, A-191 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -3,11 +3,14 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
|
||||
gitseat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/git"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"mesh-delivery-pipeline/internal/pipeline"
|
||||
@@ -39,3 +42,25 @@ func (b busStepAsker) AskStep(accept, id string, body any) error {
|
||||
}
|
||||
|
||||
var _ pipeline.StepAsker = busStepAsker{}
|
||||
|
||||
// commitAuthor reads the forge's login of the author of a commit through the seat git's verb pull, by the commit that merged it
|
||||
// (design 47 G §33, A-191): a commit no pull request merged, or a read that failed, is an error, never an empty author.
|
||||
func commitAuthor(call meshcatalogclient.Call) func(repository, commit string) (string, error) {
|
||||
return func(repository, commit string) (string, error) {
|
||||
raw, err := call("seat:"+gitseat.Seat+"."+gitseat.VerbPull, gitseat.PullInput{Repository: repository, MergeCommit: commit})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var a gitseat.PullAnswer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return "", fmt.Errorf("the answer of %s.%s is not its seat's: %w", gitseat.Seat, gitseat.VerbPull, err)
|
||||
}
|
||||
switch {
|
||||
case a.Error != "":
|
||||
return "", fmt.Errorf("%s (%s)", a.Error, a.ErrorKind)
|
||||
case a.Pull == nil || a.Pull.Author == "":
|
||||
return "", fmt.Errorf("no pull request that merged %s names its author", commit)
|
||||
}
|
||||
return a.Pull.Author, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,13 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// findingWriter is the runner's mesh-sdk finding writer (design 47 G §461, §689): the runner observes only what its views
|
||||
// reader finds, so its own observe states none and the writer's statement covers the reader's open findings. The writer
|
||||
// findingWriter is the runner's mesh-sdk held finding writer (design 47 G §461, §689, A-29): the runner raises its findings
|
||||
// as they happen (a failed head, §33), so the writer holds what it has written and its statement says exactly that, with the
|
||||
// reader's open findings. The writer
|
||||
// forms its subjects itself (design 47 G §383, A-13).
|
||||
func findingWriter(ctx context.Context, cred process.Account, conn *nats.Conn, js jetstream.JetStream, state process.State, logf func(string, ...any)) (*views.Writer[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent], error) {
|
||||
w, err := views.NewWriter(views.WriterConfig[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent]{
|
||||
func findingWriter(ctx context.Context, cred process.Account, conn *nats.Conn, js jetstream.JetStream, state process.State, logf func(string, ...any)) (*views.HeldWriter[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent], error) {
|
||||
w, err := views.NewHeldWriter(views.WriterConfig[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent]{
|
||||
Module: cred.Module, State: state, Node: cred.Node, Asked: condseat.SubjectFindingsAsked, JS: js, Conn: conn, Logf: logf,
|
||||
Observe: func(context.Context) (views.Observed[condseat.Finding], error) {
|
||||
return views.Observed[condseat.Finding]{}, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -101,6 +101,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
hearer := newHearer(ctx, js, cred, pipelines, kept, say, logger.Printf)
|
||||
hearer.Asker = busAsker{conn: conn, cred: cred}
|
||||
hearer.Where = routerRecordOf(cred.Module, calls.Call, calls.Ask)
|
||||
hearer.Failures = &pipeline.Failures{Findings: rv.Findings, Observer: cred.Module, Author: commitAuthor(calls.Call), Logf: logger.Printf}
|
||||
// Its asking half (§22, §24): a pipeline's steps are placed from the module's registration and each build seat is asked
|
||||
// for its step.
|
||||
starter := &pipeline.Starter{Pipelines: createdKV{ctx: ctx, kv: pipelines}, Logf: logger.Printf, Say: say,
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
type runnerViews struct {
|
||||
Registrations *meshcatalogclient.Registrations
|
||||
Catalog *pipeline.Views
|
||||
// Findings is the runner's held finding writer: it raises the finding pipeline-failed (§33).
|
||||
Findings pipeline.Findings
|
||||
}
|
||||
|
||||
// startViews wires the runner's views on hub and asks each first view; register every other handler on hub before or
|
||||
@@ -53,7 +55,7 @@ func startViews(ctx context.Context, js jetstream.JetStream, conn *nats.Conn, cr
|
||||
return nil, err
|
||||
}
|
||||
reader.Start(ctx, js, cred, status)
|
||||
return &runnerViews{Registrations: regs, Catalog: cv}, nil
|
||||
return &runnerViews{Registrations: regs, Catalog: cv, Findings: findings}, nil
|
||||
}
|
||||
|
||||
// catalogView is a branch's meshcatalogclient.Catalog as the runner reads it.
|
||||
|
||||
@@ -10,7 +10,9 @@ require (
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/git v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshpipelinerunnerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -84,6 +84,7 @@ func (h *Hearer) endNotTaken(key, ask string, bound time.Duration) error {
|
||||
return fmt.Errorf("pipeline %s cannot be written: %w", key, err)
|
||||
}
|
||||
h.logEnd(key, *s, now)
|
||||
h.Failures.StepEnded(h.Store, p, *s)
|
||||
if err := h.say(seat.EventStepEnded, p, s); err != nil {
|
||||
return fmt.Errorf("pipeline %s's %s is written but not said: %w", key, seat.EventStepEnded, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshpipelinerunner"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// PipelineFailedKind is the kind of the runner's finding of a failed head (design 47 G §33, §540).
|
||||
const PipelineFailedKind = "pipeline-failed"
|
||||
|
||||
// forgeOwner is the form of an owner that names a commit's author as the forge does (design 47 G §540, §678).
|
||||
var forgeOwner = regexp.MustCompile(`^forge:[A-Za-z0-9._-]+$`)
|
||||
|
||||
// Findings is the runner's finding writer as the failures use it: mesh-sdk's held writer of the seat mesh-conditions' finding.
|
||||
type Findings interface {
|
||||
Write(ctx context.Context, f condseat.Finding) error
|
||||
Clear(ctx context.Context, scope, id, kind, why string) error
|
||||
Held() []condseat.Finding
|
||||
}
|
||||
|
||||
// Failures says a failed head (design 47 G §33, §540, A-191): a `required` step that failed, or ended `could not run`, is the
|
||||
// finding `pipeline-failed` of its commit, naming the module, the branch, the commit and the steps, with the commit's author as
|
||||
// its owner `forge:<account>`; a newer revision of that branch passing each of those steps clears it. The condition keeper
|
||||
// raises it as the author's, and the operator's after two hours (§540).
|
||||
type Failures struct {
|
||||
Findings Findings
|
||||
// Observer is the runner's own module name.
|
||||
Observer string
|
||||
// Author reads the forge's login of the commit's author through the seat git (the verb pull, by the commit that merged
|
||||
// it); an error is a commit no pull request merged or a read that failed.
|
||||
Author func(repository, commit string) (string, error)
|
||||
Now func() time.Time
|
||||
Logf func(string, ...any)
|
||||
}
|
||||
|
||||
func (f *Failures) logf(format string, args ...any) {
|
||||
if f.Logf != nil {
|
||||
f.Logf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// failedSteps are the required steps of p that failed or could not run, the approval excepted: an approval's end is the
|
||||
// operator's, not a failure of the commit.
|
||||
func failedSteps(p seat.Pipeline) []seat.StepResult {
|
||||
var out []seat.StepResult
|
||||
for _, s := range p.Steps {
|
||||
if s.Status == seat.StepResultStatusRequired && s.Step != seat.StepResultStepApproval &&
|
||||
(s.Outcome == seat.StepResultOutcomeFailed || s.Outcome == seat.StepResultOutcomeCouldNotRun) {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (f *Failures) held(commit string) *condseat.Finding {
|
||||
for _, h := range f.Findings.Held() {
|
||||
if h.Scope == mcc.ScopeMerge && h.ID == commit && h.Kind == PipelineFailedKind {
|
||||
h := h
|
||||
return &h
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StepEnded is what the end of step s of pipeline p says of its head: a failed required step writes the finding of its commit,
|
||||
// a step of a commit already found failed that is not one writes its owner when it had none, and a passing required step clears
|
||||
// the findings of the older revisions it makes good.
|
||||
func (f *Failures) StepEnded(store Updater, p seat.Pipeline, s seat.StepResult) {
|
||||
if f == nil || f.Findings == nil || p.Commit == "" {
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
switch {
|
||||
case len(failedSteps(p)) > 0:
|
||||
f.write(ctx, p)
|
||||
case s.Outcome == seat.StepResultOutcomePassed || s.Outcome == seat.StepResultOutcomeCarried:
|
||||
f.clearNewer(ctx, store, p)
|
||||
}
|
||||
}
|
||||
|
||||
// write writes p's commit's finding; the owner it held stays, and one it lacked is read again here, so the owner of a
|
||||
// failed read is written at the next step end of that commit (A-191).
|
||||
func (f *Failures) write(ctx context.Context, p seat.Pipeline) {
|
||||
steps := failedSteps(p)
|
||||
var names []string
|
||||
for _, s := range steps {
|
||||
names = append(names, strings.TrimSpace(s.Artifact+" "+s.Step)+" ("+s.Outcome+")")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if f.Now != nil {
|
||||
now = f.Now().UTC()
|
||||
}
|
||||
owner, why := "", ""
|
||||
if old := f.held(p.Commit); old != nil {
|
||||
owner = old.Owner
|
||||
}
|
||||
if owner == "" {
|
||||
owner, why = f.author(p)
|
||||
}
|
||||
explanation := fmt.Sprintf("The required steps %s of %s did not pass on the branch %s at the commit %s. A newer commit of the branch that passes them clears this.",
|
||||
strings.Join(names, ", "), p.Module, p.Branch, p.Commit)
|
||||
if why != "" {
|
||||
explanation += " " + why
|
||||
}
|
||||
err := f.Findings.Write(ctx, condseat.Finding{Scope: mcc.ScopeMerge, ID: p.Commit, Kind: PipelineFailedKind, Severity: condseat.FindingSeverityWarning,
|
||||
Summary: fmt.Sprintf("%s on %s at %s: %s", p.Module, p.Branch, p.Commit, strings.Join(names, ", ")),
|
||||
Headline: fmt.Sprintf("A required step of %s failed on %s", p.Module, p.Branch),
|
||||
Explanation: explanation, Needs: "a newer commit of " + p.Branch + " that passes " + strings.Join(names, ", "),
|
||||
Owner: owner, Source: "pipeline-runner." + PipelineFailedKind, Observer: f.Observer, At: now,
|
||||
Subject: condseat.Subject{Scope: mcc.ScopeMerge, ID: p.Commit}})
|
||||
if err != nil {
|
||||
f.logf("the finding %s of %s at %s is not written: %v", PipelineFailedKind, p.Module, p.Commit, err)
|
||||
}
|
||||
}
|
||||
|
||||
// author is the finding's owner `forge:<account>` and, when it has none, why it has none (A-191).
|
||||
func (f *Failures) author(p seat.Pipeline) (string, string) {
|
||||
if f.Author == nil {
|
||||
return "", "The commit's author is not read, so the finding has no owner."
|
||||
}
|
||||
account, err := f.Author(p.Repository, p.Commit)
|
||||
if err != nil {
|
||||
f.logf("the author of %s in %s is not read, so its finding %s has no owner: %v", p.Commit, p.Repository, PipelineFailedKind, err)
|
||||
return "", "The commit's author is not known (" + err.Error() + "), so the finding has no owner."
|
||||
}
|
||||
if !forgeOwner.MatchString("forge:" + account) {
|
||||
f.logf("the author %q of %s is no forge account name, so its finding %s has no owner", account, p.Commit, PipelineFailedKind)
|
||||
return "", "The commit's author is not known as a forge account, so the finding has no owner."
|
||||
}
|
||||
return "forge:" + account, ""
|
||||
}
|
||||
|
||||
// clearNewer clears the finding of each older commit of p's module and branch whose every failed step a revision newer than it
|
||||
// has passed (§33: cleared when a newer revision of that branch passes that step).
|
||||
func (f *Failures) clearNewer(ctx context.Context, store Updater, p seat.Pipeline) {
|
||||
pkeys, err := store.Keys(keys.EscapeName(p.Branch) + ".*." + p.Module)
|
||||
if err != nil {
|
||||
f.logf("the pipelines of %s on %s are not listed, so no finding %s is cleared: %v", p.Module, p.Branch, PipelineFailedKind, err)
|
||||
return
|
||||
}
|
||||
var all []seat.Pipeline
|
||||
for _, k := range pkeys {
|
||||
if q, _, err := store.Get(k); err == nil {
|
||||
all = append(all, q)
|
||||
}
|
||||
}
|
||||
for _, old := range all {
|
||||
if old.Source >= p.Source || f.held(old.Commit) == nil {
|
||||
continue
|
||||
}
|
||||
failed := failedSteps(old)
|
||||
if len(failed) == 0 {
|
||||
continue
|
||||
}
|
||||
made := true
|
||||
for _, s := range failed {
|
||||
made = made && passedAfter(all, old, s)
|
||||
}
|
||||
if made {
|
||||
if err := f.Findings.Clear(ctx, mcc.ScopeMerge, old.Commit, PipelineFailedKind, "a newer revision of "+p.Branch+" passes its steps"); err != nil {
|
||||
f.logf("the finding %s of %s at %s is not cleared: %v", PipelineFailedKind, p.Module, old.Commit, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// passedAfter says whether a pipeline newer than old passed the step s of old.
|
||||
func passedAfter(all []seat.Pipeline, old seat.Pipeline, s seat.StepResult) bool {
|
||||
for _, q := range all {
|
||||
if q.Source <= old.Source {
|
||||
continue
|
||||
}
|
||||
for _, t := range q.Steps {
|
||||
if t.Artifact == s.Artifact && t.Step == s.Step && (t.Outcome == seat.StepResultOutcomePassed || t.Outcome == seat.StepResultOutcomeCarried) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshpipelinerunner"
|
||||
nodebuildgo "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo"
|
||||
)
|
||||
|
||||
// heldFindings is a finding writer in memory, by key.
|
||||
type heldFindings struct {
|
||||
held map[string]condseat.Finding
|
||||
cleared []string
|
||||
}
|
||||
|
||||
func newHeld() *heldFindings { return &heldFindings{held: map[string]condseat.Finding{}} }
|
||||
|
||||
func (h *heldFindings) Write(_ context.Context, f condseat.Finding) error {
|
||||
h.held[f.Scope+"."+f.ID+"."+f.Kind] = f
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *heldFindings) Clear(_ context.Context, scope, id, kind, _ string) error {
|
||||
delete(h.held, scope+"."+id+"."+kind)
|
||||
h.cleared = append(h.cleared, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *heldFindings) Held() []condseat.Finding {
|
||||
var out []condseat.Finding
|
||||
for _, f := range h.held {
|
||||
out = append(out, f)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// A required step that failed or could not run is the finding pipeline-failed of its commit, owned by the commit's author as
|
||||
// the forge records it; one whose author is not known has no owner and says why, and the next step end of that commit writes it
|
||||
// (design 47 G §33, §540, A-191); an optional step that failed says nothing.
|
||||
func TestAFailedRequiredStepIsTheFindingOfItsCommit(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name, step, outcome string
|
||||
author string
|
||||
authorErr error
|
||||
wantFinding bool
|
||||
wantOwner string
|
||||
}{
|
||||
{"required failed", "lint", "failed", "alice", nil, true, "forge:alice"},
|
||||
{"required could not run", "lint", "could not run", "alice", nil, true, "forge:alice"},
|
||||
{"no pull request merged it", "lint", "failed", "", errors.New("not found"), true, ""},
|
||||
{"optional failed", "unit", "failed", "alice", nil, false, ""},
|
||||
{"required passed", "lint", "passed", "alice", nil, false, ""},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
h, store, _, _, _, _ := fixture()
|
||||
p := store[pkey]
|
||||
p.p.Repository = "novox/app"
|
||||
store[pkey] = p
|
||||
f := newHeld()
|
||||
h.Failures = &Failures{Findings: f, Observer: "mesh-delivery-pipeline", Now: h.Now,
|
||||
Author: func(repository, commit string) (string, error) {
|
||||
if repository != "novox/app" || commit != "c1" {
|
||||
t.Errorf("the author of %s in %s was asked", commit, repository)
|
||||
}
|
||||
return c.author, c.authorErr
|
||||
}}
|
||||
ask := map[string]string{"lint": "a1", "unit": "a2"}[c.step]
|
||||
if _, err := h.OnStepEnded(goSeat, goSeat.Prefix+"n1", ended(ask, c.step, c.outcome)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := f.held["merge.c1.pipeline-failed"]
|
||||
if ok != c.wantFinding {
|
||||
t.Fatalf("finding written: %v, want %v (%v)", ok, c.wantFinding, f.held)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if got.Owner != c.wantOwner || got.Observer != "mesh-delivery-pipeline" || got.Subject.ID != "c1" {
|
||||
t.Fatalf("finding %+v, want owner %q", got, c.wantOwner)
|
||||
}
|
||||
for _, name := range []string{"app", "main", "c1", "lint"} {
|
||||
if !strings.Contains(got.Explanation, name) {
|
||||
t.Errorf("the explanation does not name %q: %s", name, got.Explanation)
|
||||
}
|
||||
}
|
||||
if c.wantOwner == "" && !strings.Contains(got.Explanation, "no owner") {
|
||||
t.Errorf("an ownerless finding does not say why: %s", got.Explanation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The owner of a failed head whose author could not be read is written at the next step end of that commit (A-191).
|
||||
func TestTheOwnerOfAFailedHeadIsWrittenAtTheNextStepEnd(t *testing.T) {
|
||||
h, store, _, _, _, _ := fixture()
|
||||
f := newHeld()
|
||||
known := false
|
||||
h.Failures = &Failures{Findings: f, Now: h.Now, Author: func(string, string) (string, error) {
|
||||
if !known {
|
||||
return "", errors.New("the forge did not answer")
|
||||
}
|
||||
return "bob", nil
|
||||
}}
|
||||
if _, err := h.OnStepEnded(goSeat, goSeat.Prefix+"n1", ended("a1", "lint", "failed")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.held["merge.c1.pipeline-failed"].Owner != "" {
|
||||
t.Fatalf("an owner before the author is known: %+v", f.held)
|
||||
}
|
||||
known = true
|
||||
p := store[pkey]
|
||||
p.p.Steps[1].Status = seat.StepResultStatusRequired
|
||||
store[pkey] = p
|
||||
if _, err := h.OnStepEnded(goSeat, goSeat.Prefix+"n1", ended("a2", "unit", "failed")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := f.held["merge.c1.pipeline-failed"].Owner; got != "forge:bob" {
|
||||
t.Fatalf("owner %q, want forge:bob", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A newer revision of the branch passing the step that failed clears the finding of the older commit; one that does not pass
|
||||
// it, and a newer head failing on its own, leave it, and the newer failing head is a finding of its own (§33, §540).
|
||||
func TestANewerPassingRevisionClearsTheFinding(t *testing.T) {
|
||||
h, store, _, _, _, _ := fixture()
|
||||
f := newHeld()
|
||||
h.Failures = &Failures{Findings: f, Now: h.Now, Author: func(string, string) (string, error) { return "alice", nil }}
|
||||
if _, err := h.OnStepEnded(goSeat, goSeat.Prefix+"n1", ended("a1", "lint", "failed")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
newer := func(source int64, commit, ask string) string {
|
||||
key := "main." + strconv.FormatInt(source, 10) + ".app"
|
||||
store[key] = struct {
|
||||
p seat.Pipeline
|
||||
rev uint64
|
||||
}{seat.Pipeline{V: 1, Pipeline: key, Branch: "main", Source: source, Module: "app", Commit: commit, Fingerprint: "src2:aa",
|
||||
Outcome: seat.PipelineOutcomeRunning, Steps: []seat.StepResult{{Artifact: "program", Step: seat.StepResultStepLint,
|
||||
Status: seat.StepResultStatusRequired, Outcome: seat.StepResultOutcomeRunning, Ask: ask, Seat: nodebuildgo.Seat}}}, 1}
|
||||
return key
|
||||
}
|
||||
// A newer head that fails the step too is a finding of its own, and the older one stays.
|
||||
key := newer(6, "c2", "b1")
|
||||
ev := ended("b1", "lint", "failed")
|
||||
ev.Pipeline, ev.Commit = key, "c2"
|
||||
if _, err := h.OnStepEnded(goSeat, goSeat.Prefix+"n1", ev); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := f.held["merge.c1.pipeline-failed"]; !ok || len(f.held) != 2 {
|
||||
t.Fatalf("findings %v, want c1 and c2", f.held)
|
||||
}
|
||||
// A newest head passing it clears both older commits' findings.
|
||||
key = newer(7, "c3", "b2")
|
||||
ev = ended("b2", "lint", "passed")
|
||||
ev.Pipeline, ev.Commit = key, "c3"
|
||||
if _, err := h.OnStepEnded(goSeat, goSeat.Prefix+"n1", ev); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(f.held) != 0 {
|
||||
t.Fatalf("findings %v stand after a newer revision passed the step", f.held)
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,8 @@ type Hearer struct {
|
||||
Where func(ask string) (string, *oc.Warrant, error)
|
||||
Logf func(string, ...any)
|
||||
Now func() time.Time
|
||||
// Failures says a failed head as the finding pipeline-failed (§33); nil says none.
|
||||
Failures *Failures
|
||||
}
|
||||
|
||||
func (h *Hearer) now() time.Time {
|
||||
@@ -189,6 +191,7 @@ func (h *Hearer) OnStepEnded(bs StepSeat, subject string, ev StepEnded) (seat.Pi
|
||||
}
|
||||
}
|
||||
h.logEnd(ev.Pipeline, *s, now)
|
||||
h.Failures.StepEnded(h.Store, p, *s)
|
||||
if err := h.say(seat.EventStepEnded, p, s); err != nil {
|
||||
return p, fmt.Errorf("pipeline %s's %s is written but not said: %w", ev.Pipeline, seat.EventStepEnded, err)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ func TestManifest(t *testing.T) {
|
||||
{"invokes", equalSets(m.Invokes, []string{"seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-hand-acts.hand-act",
|
||||
// RN3 (§53, §334): a publication's dependents behind.
|
||||
"seat:mesh-catalog.outdated",
|
||||
// RN11 (§33, A-191): the author of a failed head, read as the forge records it.
|
||||
"seat:git.pull",
|
||||
// A-74, A-89: who holds the seat operator-channel, whose state asks holds the router's record of the approval ask.
|
||||
"seat:mesh-module-assigner.assignments",
|
||||
// A-102 (§22, §24): the accepted kinds the runner puts its steps on.
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"seat:mesh-catalog.revision",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:mesh-catalog.outdated",
|
||||
"seat:git.pull",
|
||||
"seat:mesh-module-assigner.assignments",
|
||||
"seat:mesh-builds.step",
|
||||
"seat:mesh-wants.step",
|
||||
|
||||
Reference in New Issue
Block a user