mesh/merge-gate head e4acab32d6a2 merged into trunk 60cc9ec363df — pass: builds mesh-controller → novox; no bus step; every machine composes with the chang…
mesh/repo-check head e4acab32d6a2 merged into trunk 60cc9ec363df — pass: its merge-check.sh passed
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792009731456564734 (novox/mesh-controller at 60cc9ec3 into ma…
A check judges one head; once the pull request has another, its verdict answers nobody and its run holds a build machine the new head waits behind. Asking a check of R#N at H now drops every waiting check of R#N at another head and kills every one in flight, saying each in the log. Announcements are unordered and a recheck asks a recorded head, so this happens only when the forge confirms H is the pull request's current head; otherwise nothing ends and that is said. A one-shot recheck ends them within the call. Tries, walk builds, group checks, other pull requests' checks and rechecks of the same head are left alone. novox/hq issue 726
158 lines
6.3 KiB
Go
158 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A new push to a pull request ends the check of its old head (novox/hq issue 726).
|
|
//
|
|
// A check judges one head; once the pull request has another, its verdict answers nobody and its run holds a build
|
|
// machine for minutes the new head waits behind. So when a check is asked for pull request N of a repository at a
|
|
// head, **every check of that pull request at another head ends**: a waiting one is dropped from the queue as
|
|
// `cancel` drops it, one in flight is ended on its machine as `kill` ends it. Each is said in the log.
|
|
//
|
|
// **Never anything else**: a try (`try`, asked for no pull request), a walk's build (no check), a delivery group's
|
|
// composed check (it answers several pull requests), another pull request's check, a dead ask (left for `clear
|
|
// --dead`), and a check of the same head — a recheck of that head is not another head.
|
|
|
|
// **Only for the pull request's current head.** Announcements of a pull request are not ordered, one is heard again
|
|
// after a restart, and a recheck asks for the head it recorded: a check asked for a head the pull request no longer
|
|
// has must never end the check of the head it has. So the forge is asked first which head the pull request has; only
|
|
// when it names this one does anything end, and when it cannot be asked, or names another, nothing ends and that is
|
|
// said.
|
|
|
|
// forgePullHead asks the forge, through its module's tools on the bus, which commit a pull request's head is. A
|
|
// variable so a test needs no forge.
|
|
var forgePullHead = func(ctx context.Context, js *broker.JetStream, owner, repo string, number int) (string, error) {
|
|
raw, _ := json.Marshal(map[string]any{"path": fmt.Sprintf("/repos/%s/%s/pulls/%d", owner, repo, number)})
|
|
answer, err := link.Ask(ctx, link.OverNATS{Conn: js.Conn()}, "gitea", "gitea_api", raw, 30*time.Second)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if answer.Error != "" {
|
|
return "", errors.New(answer.Error)
|
|
}
|
|
var pull struct {
|
|
Result struct {
|
|
Head struct {
|
|
SHA string `json:"sha"`
|
|
} `json:"head"`
|
|
} `json:"result"`
|
|
}
|
|
if err := json.Unmarshal(answer.Result, &pull); err != nil {
|
|
return "", fmt.Errorf("the forge's answer about the pull request cannot be read: %w", err)
|
|
}
|
|
if pull.Result.Head.SHA == "" {
|
|
return "", errors.New("the forge named no head for the pull request")
|
|
}
|
|
return pull.Result.Head.SHA, nil
|
|
}
|
|
|
|
// supersededChecks are the checks in the queue that a check of owner/repo#number at head ends: of the same pull
|
|
// request, at another head, waiting or in flight, and not the ask keep.
|
|
func supersededChecks(q link.Queue, owner, repo string, number int, head, keep string) []link.QueuedAsk {
|
|
if number <= 0 || head == "" {
|
|
return nil
|
|
}
|
|
var out []link.QueuedAsk
|
|
for _, a := range q.Asks {
|
|
c := a.Request.Check
|
|
switch {
|
|
case c == nil, link.IsTry(a.Request), c.Group != "", c.Number != number, a.ID == keep,
|
|
!strings.EqualFold(c.Owner, owner), !strings.EqualFold(c.Repo, repo),
|
|
a.State != link.AskWaiting && a.State != link.AskInFlight:
|
|
continue
|
|
}
|
|
ref := a.Request.Ref
|
|
if ref == "" {
|
|
ref = a.Ref
|
|
}
|
|
if ref == "" || ref == head || link.SameHead(ref, head) {
|
|
continue
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// endSupersededChecks ends the checks a check of p at its head supersedes, keep being the new check's own ask, and
|
|
// says each in the log. A waiting one is cancelled — nothing recorded: a check builds nothing, and the forge's status
|
|
// of the old head is no longer the pull request's — and one a machine builds is killed there.
|
|
func endSupersededChecks(ctx context.Context, js *broker.JetStream, seat string, p link.PullUpdated, keep string) []string {
|
|
if p.Number <= 0 || p.Commit == "" {
|
|
return nil
|
|
}
|
|
current, err := forgePullHead(ctx, js, p.Owner, p.Repo, p.Number)
|
|
if err != nil {
|
|
return []string{fmt.Sprintf("%s/%s#%d (%.8s): nothing ended — the forge could not say the pull request's "+
|
|
"current head, so checks of its other heads keep running: %v", p.Owner, p.Repo, p.Number, p.Commit, err)}
|
|
}
|
|
if current != p.Commit && !link.SameHead(current, p.Commit) {
|
|
return []string{fmt.Sprintf("%s/%s#%d (%.8s): nothing ended — the pull request's head is %.12s now, not "+
|
|
"this one", p.Owner, p.Repo, p.Number, p.Commit, current)}
|
|
}
|
|
q, err := link.ReadQueue(ctx, js, seat)
|
|
if err != nil {
|
|
return []string{fmt.Sprintf("%s/%s#%d (%.8s): could not read the %s queue for checks of its earlier heads: %v",
|
|
p.Owner, p.Repo, p.Number, p.Commit, seat, err)}
|
|
}
|
|
var said []string
|
|
for _, a := range supersededChecks(q, p.Owner, p.Repo, p.Number, p.Commit, keep) {
|
|
old := a.Request.Ref
|
|
if old == "" {
|
|
old = a.Ref
|
|
}
|
|
what := fmt.Sprintf("check of %s/%s#%d at %.12s", p.Owner, p.Repo, p.Number, old)
|
|
var err error
|
|
if a.State == link.AskInFlight && a.On != "" {
|
|
var answer link.Answer
|
|
answer, err = link.AskSeatTool(ctx, js.Conn(), seat, "kill", a.On, map[string]string{"id": a.ID}, killAnswer)
|
|
if err == nil && answer.Error != "" {
|
|
err = fmt.Errorf("%s refused: %s", a.On, answer.Error)
|
|
}
|
|
} else {
|
|
_, err = cancelAsk(ctx, js, nil, seat, a)
|
|
}
|
|
if err != nil {
|
|
said = append(said, fmt.Sprintf("not ended: %s (%s) — superseded by %.12s, but: %v", what, a.ID, p.Commit, err))
|
|
continue
|
|
}
|
|
said = append(said, fmt.Sprintf("ended: %s — superseded by %.12s (%s, %s)", what, p.Commit, a.ID, a.State))
|
|
}
|
|
return said
|
|
}
|
|
|
|
// endingSupersededChecks ends them beside the announcement, so a kill waiting on its machine never holds the
|
|
// handler: on its own connection to the bus, within twice a kill's wait. A one-shot process (oneShot) ends them
|
|
// within the call instead, as its exit would cut them off.
|
|
func endingSupersededChecks(ctx context.Context, seat string, p link.PullUpdated, keep string, oneShot bool) {
|
|
end := func() {
|
|
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*killAnswer)
|
|
defer cancel()
|
|
js, err := dialTheBus()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s/%s#%d (%.8s): could not reach the bus to end checks of its earlier heads: %v\n",
|
|
p.Owner, p.Repo, p.Number, p.Commit, err)
|
|
return
|
|
}
|
|
defer js.Close()
|
|
for _, line := range endSupersededChecks(ctx, js, seat, p, keep) {
|
|
fmt.Println(line)
|
|
}
|
|
}
|
|
if oneShot {
|
|
end()
|
|
return
|
|
}
|
|
go end()
|
|
}
|