Pull request facts come from seat git's pull, ancestry from contains, the review time from the runner's review step, and get shows one pipeline row per module (no run where the runner's list answers none). Builds: design 47 G §654, A-75 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
156 lines
5.0 KiB
Go
156 lines
5.0 KiB
Go
package main
|
|
|
|
// A pull request's pipelines, read from the runner (design 47 G §654, A-75): the seat `mesh-pipeline-runner`'s `list`,
|
|
// given the pull request's head commit, answers that commit's pipelines, one per module of the repository it builds, and
|
|
// `show` of each answers it whole with each step's result, the review step's among them. The forge's own reviews are read
|
|
// nowhere: G keeps no review record.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// NoRun is a pull request's head commit the runner's list answers no pipeline for.
|
|
const NoRun = "no run"
|
|
|
|
// StepRow is one step of a pipeline: the step and its outcome.
|
|
type StepRow struct {
|
|
Step string `json:"step"`
|
|
Outcome string `json:"outcome"`
|
|
}
|
|
|
|
// PipelineRow is one module's pipeline of a pull request's head commit, with its steps; a commit with no pipeline is one
|
|
// row whose outcome is NoRun.
|
|
type PipelineRow struct {
|
|
PullRequest string `json:"pull-request"`
|
|
Module string `json:"module,omitempty"`
|
|
Pipeline string `json:"pipeline,omitempty"`
|
|
Outcome string `json:"outcome"`
|
|
Steps []StepRow `json:"steps,omitempty"`
|
|
// reviewed is when the review step ended in a pass or an acceptance; zero when it did not.
|
|
reviewed time.Time
|
|
}
|
|
|
|
// Pipelines reads a pull request's pipeline rows; a test stubs the asking. repository is the name in the forge's owner.
|
|
type Pipelines func(repository string, number string) ([]PipelineRow, error)
|
|
|
|
// parsePipelineKeys reads the keys of the pipelines a `list` answers.
|
|
func parsePipelineKeys(raw json.RawMessage) ([]string, error) {
|
|
m, err := answerOf(raw, "mesh-pipeline-runner.list")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var l []struct {
|
|
Pipeline string `json:"pipeline"`
|
|
}
|
|
if json.Unmarshal(m["pipelines"], &l) != nil {
|
|
return nil, fmt.Errorf("mesh-pipeline-runner.list answered no pipelines: %.200s", raw)
|
|
}
|
|
var keys []string
|
|
for _, p := range l {
|
|
if p.Pipeline != "" {
|
|
keys = append(keys, p.Pipeline)
|
|
}
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// parsePipelineRow reads the answer of `show` into a row of the pull request.
|
|
func parsePipelineRow(raw json.RawMessage, pr string) (PipelineRow, error) {
|
|
m, err := answerOf(raw, "mesh-pipeline-runner.show")
|
|
if err != nil {
|
|
return PipelineRow{}, err
|
|
}
|
|
var p struct {
|
|
Pipeline string `json:"pipeline"`
|
|
Module string `json:"module"`
|
|
Outcome string `json:"outcome"`
|
|
Steps []struct {
|
|
Step string `json:"step"`
|
|
Outcome string `json:"outcome"`
|
|
Ended *time.Time `json:"ended"`
|
|
} `json:"steps"`
|
|
}
|
|
if json.Unmarshal(m["pipeline"], &p) != nil || p.Module == "" {
|
|
return PipelineRow{}, fmt.Errorf("mesh-pipeline-runner.show answered no pipeline: %.200s", raw)
|
|
}
|
|
row := PipelineRow{PullRequest: pr, Module: p.Module, Pipeline: p.Pipeline, Outcome: p.Outcome}
|
|
for _, s := range p.Steps {
|
|
row.Steps = append(row.Steps, StepRow{Step: s.Step, Outcome: s.Outcome})
|
|
if s.Step == "review" && (s.Outcome == "passed" || s.Outcome == "accepted") && s.Ended != nil {
|
|
row.reviewed = s.Ended.UTC()
|
|
}
|
|
}
|
|
return row, nil
|
|
}
|
|
|
|
// busPipelines reads a pull request's head from the seat `git`, then its pipelines from the runner: `list` given the head
|
|
// commit, `show` of each.
|
|
func busPipelines(owner string) Pipelines {
|
|
return func(repository, number string) ([]PipelineRow, error) {
|
|
pr := repository + "#" + number
|
|
_, head, err := askPull(owner+"/"+repository, number)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
raw, err := stdio.Ask("seat:mesh-pipeline-runner.list", map[string]any{"commit": head})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keys, err := parsePipelineKeys(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(keys) == 0 {
|
|
return []PipelineRow{{PullRequest: pr, Outcome: NoRun}}, nil
|
|
}
|
|
var rows []PipelineRow
|
|
for _, k := range keys {
|
|
raw, err := stdio.Ask("seat:mesh-pipeline-runner.show", map[string]any{"pipeline": k})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
row, err := parsePipelineRow(raw, pr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
return rows, nil
|
|
}
|
|
}
|
|
|
|
// firstReview is when the earliest review step passed among the rows.
|
|
func firstReview(rows []PipelineRow) (time.Time, bool) {
|
|
var first time.Time
|
|
for _, r := range rows {
|
|
if !r.reviewed.IsZero() && (first.IsZero() || r.reviewed.Before(first)) {
|
|
first = r.reviewed
|
|
}
|
|
}
|
|
return first, !first.IsZero()
|
|
}
|
|
|
|
// pipelinesOf is the rows of every live fix of an issue: one per module's pipeline, with its steps, and NoRun where the
|
|
// runner knows none. A fix whose runner could not be read is one row saying so.
|
|
func (t *Tracker) pipelinesOf(is *Issue) []PipelineRow {
|
|
if t.Pipelines == nil {
|
|
return nil
|
|
}
|
|
var out []PipelineRow
|
|
for _, pr := range liveFixes(is) {
|
|
repo, number, _ := strings.Cut(pr, "#")
|
|
rows, err := t.Pipelines(repo, number)
|
|
if err != nil {
|
|
out = append(out, PipelineRow{PullRequest: pr, Outcome: "not readable: " + cut(err.Error(), 200)})
|
|
continue
|
|
}
|
|
out = append(out, rows...)
|
|
}
|
|
return out
|
|
}
|