vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Show run durations and lazy step logs
c765ae2e18691b3f98d93b00b4eb418ce781e3f1 · Keenan Nemetz · 13d ago · parent bdf6a0de
ci/agent/agent.go +28−7
| @@ -467,14 +467,14 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur | |||
| 467 | 467 | // A run queued before images were required has no environment to start. | |
| 468 | 468 | // Fail it with a record rather than guessing at the host. | |
| 469 | 469 | fmt.Fprintf(log, "\nthis run has no image; re-queue it from a workflow that declares image or image-file\n") | |
| 470 | s.finish(remote, co, job, log, 1, nil, nil, box) | ||
| 470 | s.finish(remote, co, job, log, 1, nil, nil, nil, box) | ||
| 471 | 471 | return | |
| 472 | 472 | } | |
| 473 | 473 | fail := func(format string, args ...any) { | |
| 474 | 474 | msg := fmt.Sprintf(format, args...) | |
| 475 | 475 | fmt.Fprintf(os.Stderr, "vole runner: %s\n", msg) | |
| 476 | 476 | fmt.Fprintf(log, "\n%s\n", msg) | |
| 477 | s.finish(remote, co, job, log, 1, nil, nil, box) | ||
| 477 | s.finish(remote, co, job, log, 1, nil, nil, nil, box) | ||
| 478 | 478 | } | |
| 479 | 479 | fmt.Fprintf(log, "=== %s · %s · %s @ %s\n", job.Repo, job.Job, job.Branch, format.SHA(job.SHA)) | |
| 480 | 480 | fmt.Fprintf(log, "environment: %s via %s\n", job.Image, box.Runtime) | |
| @@ -520,21 +520,34 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur | |||
| 520 | 520 | } | |
| 521 | 521 | }() | |
| 522 | 522 | ||
| 523 | steps := make([]runs.StepResult, 0, len(job.Steps)) | ||
| 523 | 524 | for _, step := range job.Steps { | |
| 525 | // The step's section of the log starts at its header, so a reader can open | ||
| 526 | // one step without holding the image build that ran before it. | ||
| 527 | logStart := int64(log.Len()) | ||
| 528 | started := time.Now().UTC() | ||
| 524 | 529 | fmt.Fprintf(log, "\n--- %s\n", step.Name) | |
| 525 | 530 | code, chunk, lost := s.execStep(job.ID, sandbox, step, lease) | |
| 526 | 531 | log.WriteString(chunk) | |
| 532 | result := runs.StepResult{ | ||
| 533 | Name: step.Name, Started: started, Finished: time.Now().UTC(), Exit: code, | ||
| 534 | LogOffset: logStart, LogSize: int64(log.Len()) - logStart, | ||
| 535 | } | ||
| 527 | 536 | if lost { | |
| 528 | 537 | fmt.Fprintf(os.Stderr, "vole runner: lease lost while running %q; stopping\n", step.Name) | |
| 529 | 538 | return // no finish: the queue already gave it to somebody else | |
| 530 | 539 | } | |
| 531 | 540 | if code != 0 { | |
| 532 | 541 | fmt.Fprintf(log, "\nstep %q exited %d\n", step.Name, code) | |
| 542 | result.LogSize = int64(log.Len()) - logStart | ||
| 543 | result.Finished = time.Now().UTC() | ||
| 544 | steps = append(steps, result) | ||
| 533 | 545 | sandbox.Stop() | |
| 534 | 546 | sandbox = nil | |
| 535 | s.finish(remote, co, job, log, code, nil, nil, box) | ||
| 547 | s.finish(remote, co, job, log, code, steps, nil, nil, box) | ||
| 536 | 548 | return | |
| 537 | 549 | } | |
| 550 | steps = append(steps, result) | ||
| 538 | 551 | } | |
| 539 | 552 | // The steps are done. Stop the container before reading artifacts or deploying a | |
| 540 | 553 | // site, so a process the workflow left in the background cannot change the files | |
| @@ -558,7 +571,7 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur | |||
| 558 | 571 | fail("%s", err) | |
| 559 | 572 | return | |
| 560 | 573 | } | |
| 561 | s.finish(remote, co, job, log, 0, arts, releaseArts, box) | ||
| 574 | s.finish(remote, co, job, log, 0, steps, arts, releaseArts, box) | ||
| 562 | 575 | } | |
| 563 | 576 | ||
| 564 | 577 | // cacheFor returns the mount for one job's cache, or nothing when the runner has | |
| @@ -596,11 +609,18 @@ func (s *runnerSession) cacheFor(job *runnerJob) []container.Mount { | |||
| 596 | 609 | // A log that cannot be stored does not lose the result: the record then names no log | |
| 597 | 610 | // and the failure is reported on stderr rather than swallowed. A record that cannot be | |
| 598 | 611 | // pushed is not reported at all, because there is nothing for the server to read. | |
| 599 | func (s *runnerSession) finish(remote string, co checkout, job *runnerJob, log *strings.Builder, exit int, arts, releaseArts []runnerArtifact, box runnerBox) { | ||
| 612 | func (s *runnerSession) finish(remote string, co checkout, job *runnerJob, log *strings.Builder, exit int, steps []runs.StepResult, arts, releaseArts []runnerArtifact, box runnerBox) { | ||
| 600 | 613 | oid, size, err := uploadLog(remote, job.Repo, log.String()) | |
| 601 | 614 | if err != nil { | |
| 602 | 615 | fmt.Fprintf(os.Stderr, "vole runner: could not upload the log for %s: %v\n", job.ID, err) | |
| 603 | 616 | } | |
| 617 | if oid == "" { | ||
| 618 | // The step ranges describe bytes in a log this attempt does not have. Keep | ||
| 619 | // the timings, which are still true, and drop the claims about the log. | ||
| 620 | for i := range steps { | ||
| 621 | steps[i].LogOffset, steps[i].LogSize = 0, 0 | ||
| 622 | } | ||
| 623 | } | ||
| 604 | 624 | artifacts := make([]runs.Artifact, 0, len(arts)) | |
| 605 | 625 | for _, a := range arts { | |
| 606 | 626 | artifacts = append(artifacts, runs.Artifact{Name: a.Name, OID: a.OID, Size: a.Size}) | |
| @@ -609,7 +629,7 @@ func (s *runnerSession) finish(remote string, co checkout, job *runnerJob, log * | |||
| 609 | 629 | RunID: job.ID, Attempt: job.Attempt, Repo: job.Repo, Ref: job.Ref, SHA: job.SHA, | |
| 610 | 630 | Workflow: job.Workflow, WorkflowSHA: job.WorkflowSHA, Job: job.Job, | |
| 611 | 631 | StepsSHA: job.StepsSHA, Exit: exit, Finished: time.Now().UTC(), | |
| 612 | Signer: s.name, Artifacts: artifacts, | ||
| 632 | Signer: s.name, Steps: steps, Artifacts: artifacts, | ||
| 613 | 633 | // The machine this attempt ran on. The runner binary's own build target, | |
| 614 | 634 | // which is the honest thing it can say: a live probe would add a subprocess | |
| 615 | 635 | // and a platform quirk for a fact the binary already knows. | |
| @@ -902,7 +922,8 @@ func collectArtifactFiles(root string, patterns []string) ([]artifactFile, error | |||
| 902 | 922 | ||
| 903 | 923 | // logBuffer collects a step's output. It is a sync buffer because os/exec copies | |
| 904 | 924 | // stdout and stderr in separate goroutines. Nothing is shipped while the step runs: | |
| 905 | // the whole log is uploaded once, when the attempt is over. | ||
| 925 | // the whole attempt's log is uploaded once, when the attempt is over, and the step | ||
| 926 | // records byte ranges into it rather than uploading a second object. | ||
| 906 | 927 | type logBuffer struct { | |
| 907 | 928 | mu sync.Mutex | |
| 908 | 929 | buf strings.Builder | |