fun_ci 2.1.1-x86_64-linux → 2.3.0-x86_64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -0
- data/README.md +36 -6
- data/lib/fun_ci/agent/cancel_command.rb +52 -0
- data/lib/fun_ci/agent/command_support.rb +7 -0
- data/lib/fun_ci/agent/commands.rb +4 -1
- data/lib/fun_ci/agent/due_in.rb +21 -0
- data/lib/fun_ci/agent/events_command.rb +14 -7
- data/lib/fun_ci/agent/job_events.rb +56 -0
- data/lib/fun_ci/agent/job_json.rb +25 -0
- data/lib/fun_ci/agent/job_report.rb +89 -0
- data/lib/fun_ci/agent/job_why.rb +59 -0
- data/lib/fun_ci/agent/job_why_json.rb +31 -0
- data/lib/fun_ci/agent/job_why_text.rb +59 -0
- data/lib/fun_ci/agent/jobs_command.rb +43 -0
- data/lib/fun_ci/agent/jobs_text.rb +55 -0
- data/lib/fun_ci/agent/live_pipeline.rb +14 -6
- data/lib/fun_ci/agent/options.rb +3 -3
- data/lib/fun_ci/agent/output.rb +14 -2
- data/lib/fun_ci/agent/snapshots.rb +20 -1
- data/lib/fun_ci/agent/span.rb +24 -0
- data/lib/fun_ci/agent/stage_summary.rb +10 -6
- data/lib/fun_ci/agent/starts_in.rb +14 -0
- data/lib/fun_ci/agent/status_command.rb +3 -2
- data/lib/fun_ci/agent/status_json.rb +1 -1
- data/lib/fun_ci/agent/status_text.rb +32 -6
- data/lib/fun_ci/agent/trunk_reading.rb +2 -1
- data/lib/fun_ci/agent/wait_command.rb +2 -1
- data/lib/fun_ci/agent/why_command.rb +6 -2
- data/lib/fun_ci/cli.rb +1 -1
- data/lib/fun_ci/cli_help.rb +4 -1
- data/lib/fun_ci/console/board_data.rb +26 -13
- data/lib/fun_ci/console/console_state.rb +12 -6
- data/lib/fun_ci/console/job_message.rb +39 -0
- data/lib/fun_ci/console/job_order.rb +23 -0
- data/lib/fun_ci/console/job_rows.rb +21 -0
- data/lib/fun_ci/console/key_handler.rb +35 -14
- data/lib/fun_ci/console/paging.rb +26 -0
- data/lib/fun_ci/console/view.rb +18 -9
- data/lib/fun_ci/evidence/settings.rb +15 -8
- data/lib/fun_ci/evidence/settings_check.rb +14 -3
- data/lib/fun_ci/jobs/due.rb +34 -0
- data/lib/fun_ci/jobs/due_jobs.rb +50 -0
- data/lib/fun_ci/jobs/folders.rb +50 -0
- data/lib/fun_ci/jobs/job.rb +31 -0
- data/lib/fun_ci/jobs/job_fork.rb +52 -0
- data/lib/fun_ci/jobs/job_run.rb +104 -0
- data/lib/fun_ci/jobs/locks.rb +30 -0
- data/lib/fun_ci/jobs/schedule.rb +25 -0
- data/lib/fun_ci/jobs/site.rb +17 -0
- data/lib/fun_ci/jobs/standings.rb +50 -0
- data/lib/fun_ci/jobs/state.rb +27 -0
- data/lib/fun_ci/jobs/wall_clock_wait.rb +29 -0
- data/lib/fun_ci/persistence/active_jobs.rb +60 -0
- data/lib/fun_ci/persistence/database.rb +14 -4
- data/lib/fun_ci/persistence/job_recorder.rb +47 -0
- data/lib/fun_ci/persistence/job_runs.rb +80 -0
- data/lib/fun_ci/persistence/pipeline_run.rb +2 -1
- data/lib/fun_ci/persistence/raw_outputs.rb +3 -0
- data/lib/fun_ci/pipeline/budgets.rb +9 -0
- data/lib/fun_ci/pipeline/pipeline_forker.rb +42 -10
- data/lib/fun_ci/pipeline/priorities.rb +23 -0
- data/lib/fun_ci/pipeline/process_runner.rb +18 -6
- data/lib/fun_ci/pipeline/progress_reporter.rb +4 -0
- data/lib/fun_ci/pipeline/run_canceller.rb +24 -0
- data/lib/fun_ci/pipeline/slot.rb +12 -1
- data/lib/fun_ci/pipeline/slot_run.rb +19 -4
- data/lib/fun_ci/pipeline/stage_execution.rb +9 -4
- data/lib/fun_ci/pipeline/trigger.rb +17 -6
- data/lib/fun_ci/pipeline/trigger_command.rb +12 -2
- data/lib/fun_ci/pipeline/trigger_params.rb +20 -9
- data/lib/fun_ci/pipeline/worktree_pool.rb +2 -2
- data/lib/fun_ci/pipeline/worktree_prune.rb +8 -6
- data/lib/fun_ci/pipeline/worktrees.rb +11 -5
- data/lib/fun_ci/setup/agent_instructions.rb +24 -5
- data/lib/fun_ci/setup/commands.rb +3 -1
- data/lib/fun_ci/setup/hook_script.rb +9 -4
- data/lib/fun_ci/setup/hook_writer.rb +13 -5
- data/lib/fun_ci/setup/installed_hooks.rb +45 -0
- data/lib/fun_ci/setup/installer.rb +3 -2
- data/lib/fun_ci/setup/lfs_hook.rb +50 -0
- data/lib/fun_ci/setup/project_config.rb +13 -1
- data/lib/fun_ci/setup/settings.rb +40 -11
- data/lib/fun_ci/setup/setup_checker.rb +20 -3
- data/lib/fun_ci/trunk/shown.rb +7 -4
- data/lib/fun_ci/version.rb +1 -1
- data/libexec/fun-ci-renderer +0 -0
- metadata +36 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 312bfad226919d4599a0bc5cacecef1c26f08dda6d522f2a181b0b8c0c39ef81
|
|
4
|
+
data.tar.gz: 1183694930f2aa2b655238523c2068e9278e51ecad3cf14be1c8065ad285d271
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 291d5610ff766c5c09903a53d0aac5eb7b7e376d4c36e39ac11cd7024a2081d763d8bcbe25312008c520bf250c124b12d0fce022904e86cced70671714fe14b4
|
|
7
|
+
data.tar.gz: 7b6524c10cbef0919c017e3ba0f784313c009c3c0bf6a0b19655f2f017ddcc148daaa1b402210e4ee579e528d7594b095aa7fd32ed56eaad455383e804ccbaa6
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,101 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.3.0] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- A commit's due daily and weekly jobs take turns instead of starting
|
|
14
|
+
together, spread evenly over the day: with four jobs, the first at once
|
|
15
|
+
and each next one six hours later, and none sooner than that after a job
|
|
16
|
+
still running or waiting. `job_spacing:` in `.fun-ci/config` sets the gap
|
|
17
|
+
instead (`30m`, `1h`; `0` starts them together). A job waiting its turn
|
|
18
|
+
says when it starts: `starts in 6h` in the console and in `fun-ci jobs`,
|
|
19
|
+
`starts_at` in its JSON, `scheduled, starts in 6h` in `status`,
|
|
20
|
+
`job_scheduled` in `events`. `c` in the console cancels it at
|
|
21
|
+
once, as does `fun-ci cancel --job`. `fun-ci check` names a spacing it
|
|
22
|
+
can't read. The console needs renderer 2.3.0 to show a waiting job; an
|
|
23
|
+
older one says it is in a state it doesn't know.
|
|
24
|
+
- `fun-ci cancel --job NAME` stops a daily or weekly job's run, as `c` on its
|
|
25
|
+
row does in the console, so an agent can too. The job runs again on the
|
|
26
|
+
next commit.
|
|
27
|
+
- `fun-ci-renderer --version` and `--help`, for anyone who installed the
|
|
28
|
+
renderer with cargo. Before, each said it "needs a value".
|
|
29
|
+
- `fun-ci check` warns when the post-commit or pre-push hook doesn't run
|
|
30
|
+
fun-ci, instead of saying all is well while no commit is tested.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- In a worktree fun-ci has just made, as for the first runs after install or
|
|
34
|
+
`fun-ci prune`, lint and build get the slow suite's budget, since their
|
|
35
|
+
caches are empty: a cold build over 30 seconds no longer blocks the first
|
|
36
|
+
push. A run in the foreground says so, `fun-ci status` says `(new
|
|
37
|
+
worktree, budget 300s)` on their lines, and one that still runs over shows
|
|
38
|
+
the longer budget in `fun-ci why`. `status --json` gives each stage's
|
|
39
|
+
`budget`.
|
|
40
|
+
- Daily and weekly jobs run at a lower priority than the stages: on macOS
|
|
41
|
+
under `taskpolicy -c utility`, elsewhere at `nice -n 19`. Before, four due
|
|
42
|
+
jobs could leave a build a fraction of the cores and push it over its
|
|
43
|
+
budget. The slow suite runs at `nice -n 10` beside the fast suite, except
|
|
44
|
+
on macOS, where nice has no measurable effect.
|
|
45
|
+
- `fun-ci init` brings the fun-ci section of `AGENTS.md` (or `CLAUDE.md`) up
|
|
46
|
+
to date, so agents in a project set up with an older fun-ci learn about
|
|
47
|
+
jobs and the trunk. It replaces the section from its `## fun-ci` heading to
|
|
48
|
+
the next heading; before, it left any section it found as it was.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- `fun-ci trigger` given a short SHA, or a revision such as `HEAD`, kept the
|
|
52
|
+
run under what it was given, so `fun-ci status`, `wait` and `why`, which
|
|
53
|
+
look runs up by the full SHA, never found it. Runs are kept under the full
|
|
54
|
+
SHA, and the daily and weekly jobs a run starts test that commit too.
|
|
55
|
+
- An agent that waited on each commit, as `fun-ci init` tells it to, kept
|
|
56
|
+
every run it made from being cancelled by the next, so their slow suites
|
|
57
|
+
ran side by side. `wait --follow-branch` no longer keeps its run going, and
|
|
58
|
+
the command a commit prints, which agents are told to run, follows the
|
|
59
|
+
branch. Run `fun-ci init` again to bring the section it wrote into
|
|
60
|
+
AGENTS.md up to date; the old one tells agents to act on `4 superseded`.
|
|
61
|
+
- `fun-ci status` said `trunk unknown, the check never finished` for a run
|
|
62
|
+
whose lint, build or fast suite took longer than 25 seconds, while the
|
|
63
|
+
check was only waiting for them to end before it was recorded.
|
|
64
|
+
- In a Git LFS repository, `fun-ci install-hooks` installed nothing, since
|
|
65
|
+
git-lfs had written post-commit and pre-push already, and no commit was
|
|
66
|
+
tested. It now writes hooks that run fun-ci's and then git-lfs's, giving
|
|
67
|
+
git-lfs the push's ref list as git sent it. In such a repository, run
|
|
68
|
+
`fun-ci install-hooks` again. `git lfs install` then says the hooks exist
|
|
69
|
+
and exits 2; they already run git-lfs's.
|
|
70
|
+
- A mistake `fun-ci check` names in `.fun-ci/config` (a wrong
|
|
71
|
+
`worktree_slots` or `job_spacing`, or YAML it can't read) no longer stops a
|
|
72
|
+
commit's run, which let the push through untested: the trigger names it,
|
|
73
|
+
the setting takes its default, and the run goes ahead. `fun-ci check` still
|
|
74
|
+
fails on it.
|
|
75
|
+
- YAML anchors and aliases in `.fun-ci/config` are read, so stages can share
|
|
76
|
+
one list of evidence entries. Before, an alias made `fun-ci check` and
|
|
77
|
+
`fun-ci trigger` die with a stack trace, and the evidence settings were
|
|
78
|
+
quietly dropped. YAML that holds a value settings can't (a date, say) is
|
|
79
|
+
named as the file's mistake instead of raised.
|
|
80
|
+
|
|
81
|
+
## [2.2.0] - 2026-10-01
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
- Daily and weekly jobs, for checks too long for a stage's budget. A script
|
|
85
|
+
in `.fun-ci/daily/` or `.fun-ci/weekly/` runs at most once a day or once a
|
|
86
|
+
week, started by your commits, in a worktree of its own, with 24 hours to
|
|
87
|
+
finish. The console shows them in a section of their own under the table,
|
|
88
|
+
and `c` cancels a running one; on a screen too short for both, the jobs
|
|
89
|
+
give way to the branches. `fun-ci check` lists them.
|
|
90
|
+
- The console's job section needs renderer 2.2.0, which the platform gems
|
|
91
|
+
bring. With an older renderer, as `cargo install` left it, the console
|
|
92
|
+
works but shows no jobs.
|
|
93
|
+
- `fun-ci prune` removes the jobs' worktrees too.
|
|
94
|
+
- Agents ask about jobs: `fun-ci jobs` lists them, `fun-ci why --job NAME`
|
|
95
|
+
prints what was kept about one's last run, `fun-ci status` names the jobs
|
|
96
|
+
that tested the commit and any failing on another, and `fun-ci events`
|
|
97
|
+
says when a job starts and finishes. The instructions `fun-ci init` writes
|
|
98
|
+
for agents mention them.
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
- On a short screen, a project whose branches had all passed and were left
|
|
102
|
+
out (counted beside the keys) kept its label, with nothing under it. The
|
|
103
|
+
label now goes with them.
|
|
104
|
+
|
|
10
105
|
## [2.1.1] - 2026-09-30
|
|
11
106
|
|
|
12
107
|
### Fixed
|
data/README.md
CHANGED
|
@@ -21,7 +21,7 @@ cd your-project
|
|
|
21
21
|
fun-ci init --everything
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
This detects your project type, writes four stage scripts into `.fun-ci/`, installs a `post-commit` and a `pre-push` git hook, and checks the setup. It also adds a short section to your `AGENTS.md` (or `CLAUDE.md`, if that is the only one) telling coding agents what to do after a commit.
|
|
24
|
+
This detects your project type, writes four stage scripts into `.fun-ci/`, installs a `post-commit` and a `pre-push` git hook, and checks the setup. It also adds a short section to your `AGENTS.md` (or `CLAUDE.md`, if that is the only one) telling coding agents what to do after a commit; run again after an upgrade, it brings that section up to date.
|
|
25
25
|
|
|
26
26
|
`fun-ci init` has templates for Ruby, Gradle, Maven, Rust, Go, Elixir, Dart, Swift, PHP, .NET, Python, Deno, Bun, Node, Perl, and C or C++ built with CMake or make. [docs/stacks.md](docs/stacks.md) shows how it recognises each one and the commands it writes. Fun-CI works with any project: the stages are shell scripts, so edit them to run whatever your project uses.
|
|
27
27
|
|
|
@@ -49,10 +49,31 @@ A stage that overruns its budget is killed and reported as over budget, in yello
|
|
|
49
49
|
|
|
50
50
|
After each commit, the `post-commit` hook starts the pipeline in the background and returns at once, so a commit is never held up. The `pre-push` hook waits for the verdict of each commit you push and stops the push if lint, build or the fast suite failed. A commit whose run has already finished goes straight through. If fun-ci isn't installed, both hooks say so and let git carry on.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
`install-hooks` leaves a hook another tool wrote alone, and `fun-ci check` warns that no commit is tested until that hook calls fun-ci. Git LFS's own hooks are the exception: fun-ci writes its hooks in their place, and they run git-lfs's after fun-ci's, so LFS objects still reach the remote. After that, `git lfs install` says the hooks exist and exits 2, since it accepts no hook but its own; there is nothing for it to do. `git lfs update --force` would put git-lfs's back, leaving fun-ci out, until you run `fun-ci install-hooks` again.
|
|
53
|
+
|
|
54
|
+
Each run happens in a git worktree of its own under `.git/fun-ci/worktrees/`, checked out at the commit it tests, so you can keep editing while it runs. Ignored files such as `vendor/bundle` or `node_modules` stay in a worktree from one run to the next, which keeps builds quick. A worktree fun-ci has just made has none of them yet, so in its first run lint and build get the slow suite's five minutes rather than 30 seconds, and `fun-ci status` says so on their lines. Two runs can go at once; set `worktree_slots: 3` in `.fun-ci/config` for more, and `fun-ci prune` removes the worktrees when you want the space back. A newer commit on a branch cancels the older run that is still going, unless an agent is waiting on it without `--follow-branch`.
|
|
53
55
|
|
|
54
56
|
Results are kept in SQLite under `$XDG_STATE_HOME/fun-ci/` (`~/.local/state/fun-ci/` by default), shared by every project on the machine.
|
|
55
57
|
|
|
58
|
+
## Daily and weekly jobs
|
|
59
|
+
|
|
60
|
+
Some checks take longer than the time between two commits: a mutation run, a soak test, a full audit of your dependencies. Put each in a script of its own under `.fun-ci/daily/` or `.fun-ci/weekly/`:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
.fun-ci/daily/mutation.sh
|
|
64
|
+
.fun-ci/weekly/soak.sh
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The script's name is the job's name. Like a stage, it gets the commit's hash as its first argument and passes when it exits 0, and `FUN_CI_JOB` tells it which job it is. There is nothing else to configure.
|
|
68
|
+
|
|
69
|
+
Commits start jobs. When the `post-commit` hook runs a pipeline, it also starts the project's due jobs on the commit just made: a job is due when it has never run, when its last run was cancelled, or a day (daily) or a week (weekly) after its last run started, so a project nobody commits to runs none. The jobs take turns, spread evenly over the day: with four jobs, the first starts at once and each next one six hours later. `job_spacing: 30m` in `.fun-ci/config` sets the gap instead, and `0` starts them together. A job waiting its turn says when it starts.
|
|
70
|
+
|
|
71
|
+
Each job runs once at a time, in a worktree of its own under `.git/fun-ci/jobs/`, at a lower priority than the stages, so it takes only the cores they leave idle and never holds up a commit's run. It has 24 hours. A newer commit never cancels a job, and a job's result changes no run, no streak and no exit code. The [design](docs/design.md#daily-and-weekly-jobs) has the rest of the rules.
|
|
72
|
+
|
|
73
|
+
A failed job keeps its evidence as a failed stage does; add entries under `evidence: jobs: <name>:` in `.fun-ci/config` to keep more. `fun-ci check` lists the jobs it found, and says which scripts it can't run.
|
|
74
|
+
|
|
75
|
+
`fun-ci jobs` lists the jobs, each with how its last run went, on which commit, and when it is due again. `fun-ci why --job soak` prints everything kept about the job's last run (`--raw` for its whole output, `--json` for a document), and exits as `why` does for a stage. `fun-ci status` names the jobs whose last run tested the commit you ask about, and any job failing on another commit, and `fun-ci events` says when a job is scheduled, starts and finishes. `fun-ci cancel --job soak` stops the job's run, as `c` does in the console, so it runs again on the next commit. None of them changes a commit's verdict. A job's name is letters, digits, `.`, `_` and `-`, so those commands can be pasted as they are printed.
|
|
76
|
+
|
|
56
77
|
## Watching: the console
|
|
57
78
|
|
|
58
79
|
```bash
|
|
@@ -88,12 +109,16 @@ Five minutes after the latest run finished, the header goes quiet: a starry nigh
|
|
|
88
109
|
|---|---|
|
|
89
110
|
|  |  |
|
|
90
111
|
|
|
112
|
+
The daily and weekly jobs have a section of their own under the last project: each job's name, how often it runs, its mark, what its last run did on which branch and commit, and when it is due again (`passed on main 3a1f9c2 · due in 14h`), or that it runs on your next commit. When none of them failed or runs, the section folds into one pale line.
|
|
113
|
+
|
|
114
|
+

|
|
115
|
+
|
|
91
116
|
A branch that conflicts with the trunk says so on the line under its name, `conflicts with main`, and the header plays two strands braiding into a knot; when a later commit on the branch merges cleanly, the knot unties. A project whose trunk fun-ci last fetched over an hour ago, or couldn't fetch, says so beside its name.
|
|
92
117
|
|
|
93
118
|
Keys:
|
|
94
119
|
|
|
95
120
|
- `j` and `k`, or the arrow keys, move the cursor down and up
|
|
96
|
-
- `c` cancels the run under the cursor: one waiting to start at once, a running one once you answer `y` (`n` or `Esc` keeps it running); the footer offers it only while a run is running or waits to start
|
|
121
|
+
- `c` cancels the run under the cursor: one waiting to start at once, a running one once you answer `y` (`n` or `Esc` keeps it running); the footer offers it only while a run is running or waits to start. The cursor moves on past the last branch into the jobs, and `c` cancels a job too: one waiting its turn at once, a running one the same way; a cancelled job runs again on your next commit
|
|
97
122
|
- `q` quits
|
|
98
123
|
|
|
99
124
|
The console is drawn in 24-bit colour when `COLORTERM` says the terminal has it (`truecolor` or `24bit`), and in 256 colours otherwise. The pictures above are the renderer's output replayed in a terminal emulator, set in Menlo; in your terminal the text is in your terminal's font.
|
|
@@ -105,10 +130,10 @@ The drawing is done by a separate program, `fun-ci-renderer`, written in Rust. T
|
|
|
105
130
|
Every commit's output ends with the command that gets its verdict:
|
|
106
131
|
|
|
107
132
|
```
|
|
108
|
-
fun-ci: testing 3f9c2ab. Verdict: fun-ci wait 3f9c2ab --need all
|
|
133
|
+
fun-ci: testing 3f9c2ab. Verdict: fun-ci wait 3f9c2ab --need all --follow-branch
|
|
109
134
|
```
|
|
110
135
|
|
|
111
|
-
The section `fun-ci init` writes into `AGENTS.md` tells an agent to run that command in the background after each commit. Agent harnesses wake the agent when a background command exits, so it hears about a failure without having to remember to ask.
|
|
136
|
+
The section `fun-ci init` writes into `AGENTS.md` tells an agent to run that command in the background after each commit. Agent harnesses wake the agent when a background command exits, so it hears about a failure without having to remember to ask. With `--follow-branch`, a newer commit on the branch cancels the older run, and the wait moves on to the newer run's verdict.
|
|
112
137
|
|
|
113
138
|
`status` says where a commit's run stands and `wait` blocks until it is decided. Both exit with the verdict for the stages you need: `--need build` (lint and build), `fast` (the default) or `all` (the slow suite too).
|
|
114
139
|
|
|
@@ -210,6 +235,8 @@ evidence:
|
|
|
210
235
|
on: overrun
|
|
211
236
|
```
|
|
212
237
|
|
|
238
|
+
Beside the entries, `evidence:` takes a few settings: `budget: 5` gives the extractors five seconds rather than two, `detect: false` stops fun-ci choosing presets from what the output shows, `skip: [rspec]` leaves out the presets named, `mask:` adds patterns of your own to mask, and `masking: false` turns masking off.
|
|
239
|
+
|
|
213
240
|
`fun-ci extract fast --output saved.log` runs a stage's extractors against a saved output (`fun-ci why --raw > saved.log`), so you can try an entry without making a commit. A mistake under `evidence:` never stops a pipeline: `fun-ci check` reports it and `fun-ci why` names it.
|
|
214
241
|
|
|
215
242
|
Secrets in the stage's environment are masked before anything is kept: the value of any variable whose name holds TOKEN, SECRET, PASSWORD, PASSWD, API_KEY, PRIVATE_KEY or CREDENTIAL, and GitHub, AWS and Slack tokens, private keys and `Authorization:` headers wherever they appear. The state directory is readable by your user alone.
|
|
@@ -224,9 +251,12 @@ fun-ci install-hooks post-commit Install a single hook type
|
|
|
224
251
|
fun-ci check Verify .fun-ci/ setup
|
|
225
252
|
fun-ci console Watch the runs
|
|
226
253
|
fun-ci status [commit] [--trunk] Where a commit's run stands; the verdict is the exit code
|
|
227
|
-
fun-ci wait [commit]
|
|
254
|
+
fun-ci wait [commit] [--follow-branch] Wait until that verdict is decided, then exit with it
|
|
228
255
|
fun-ci why [commit] [stage|trunk] Everything kept about why a stage failed, or the conflict with the trunk
|
|
229
256
|
fun-ci runs This project's recent runs, newest first
|
|
257
|
+
fun-ci jobs This project's daily and weekly jobs and how each stands
|
|
258
|
+
fun-ci why --job <name> Everything kept about a job's latest run
|
|
259
|
+
fun-ci cancel --job <name> Stop a job's run; it runs again on the next commit
|
|
230
260
|
fun-ci events [--follow] The runs' events as JSON lines
|
|
231
261
|
fun-ci extract <stage> --output <file> Try a stage's extractors on a saved output
|
|
232
262
|
fun-ci trigger <commit> <branch> Run the whole pipeline in the foreground
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "command_support"
|
|
4
|
+
require_relative "../jobs/folders"
|
|
5
|
+
require_relative "../persistence/active_jobs"
|
|
6
|
+
|
|
7
|
+
module FunCi
|
|
8
|
+
module Agent
|
|
9
|
+
# `fun-ci cancel --job NAME` (acceptance-tests.md, AT-13.26): stops a
|
|
10
|
+
# daily or weekly job's running run, as `c` on its row in the console
|
|
11
|
+
# does, and records it cancelled, so it is due again on the next commit.
|
|
12
|
+
class CancelCommand
|
|
13
|
+
include CommandSupport
|
|
14
|
+
|
|
15
|
+
NAME = "cancel"
|
|
16
|
+
|
|
17
|
+
def initialize(context)
|
|
18
|
+
@context = context
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run(args)
|
|
22
|
+
name = Options.parse(args, takes: %i[job]).job || raise(Options::Invalid, "name the job: --job NAME")
|
|
23
|
+
raise Options::Invalid, no_job(name) unless names.include?(name)
|
|
24
|
+
|
|
25
|
+
cancel(name, Persistence::ActiveJobs.running_of(@context.db, project, name))
|
|
26
|
+
rescue Options::Invalid => e
|
|
27
|
+
usage(e.message)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def cancel(name, running)
|
|
33
|
+
return say("job #{name} is not running, so there is nothing to cancel.") if running.empty?
|
|
34
|
+
|
|
35
|
+
running.each { |id| @context.pipeline.cancel_job(@context.db, id) }
|
|
36
|
+
say("cancelled job #{name}; it runs again on the next commit.")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def say(message)
|
|
40
|
+
@context.io.stdout.puts "fun-ci: #{message}"
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def project = @context.git.toplevel
|
|
45
|
+
def names = Jobs::Folders.new(project).jobs.map(&:name)
|
|
46
|
+
|
|
47
|
+
def no_job(name)
|
|
48
|
+
names.empty? ? JobWhy::NO_JOBS : "no job '#{name}' in this project: its jobs are #{names.join(", ")}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -4,6 +4,7 @@ require_relative "options"
|
|
|
4
4
|
require_relative "reports"
|
|
5
5
|
require_relative "exit_code"
|
|
6
6
|
require_relative "trunk_verdict"
|
|
7
|
+
require_relative "job_report"
|
|
7
8
|
|
|
8
9
|
module FunCi
|
|
9
10
|
module Agent
|
|
@@ -31,6 +32,12 @@ module FunCi
|
|
|
31
32
|
|
|
32
33
|
def reports = @reports ||= Reports.new(@context.db, @context.git, @context.clock, @context.trunk)
|
|
33
34
|
|
|
35
|
+
# After recording failed the jobs whose process died, which would otherwise read as running.
|
|
36
|
+
def job_reports
|
|
37
|
+
@context.pipeline.watch(@context.db)
|
|
38
|
+
JobReports.new(@context.db, @context.git.toplevel, @context.clock)
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
def usage(message)
|
|
35
42
|
@context.io.stderr.puts "fun-ci #{self.class::NAME}: #{message}"
|
|
36
43
|
ExitCode::USAGE
|
|
@@ -10,6 +10,8 @@ require_relative "runs_command"
|
|
|
10
10
|
require_relative "wait_command"
|
|
11
11
|
require_relative "events_command"
|
|
12
12
|
require_relative "why_command"
|
|
13
|
+
require_relative "jobs_command"
|
|
14
|
+
require_relative "cancel_command"
|
|
13
15
|
require_relative "../trunk/local"
|
|
14
16
|
|
|
15
17
|
module FunCi
|
|
@@ -18,7 +20,8 @@ module FunCi
|
|
|
18
20
|
# database and the project's git.
|
|
19
21
|
module Commands
|
|
20
22
|
ALL = { "status" => StatusCommand, "runs" => RunsCommand, "wait" => WaitCommand,
|
|
21
|
-
"events" => EventsCommand, "why" => WhyCommand
|
|
23
|
+
"events" => EventsCommand, "why" => WhyCommand, "jobs" => JobsCommand,
|
|
24
|
+
"cancel" => CancelCommand }.freeze
|
|
22
25
|
|
|
23
26
|
def self.run(name, args, context)
|
|
24
27
|
ALL.fetch(name).new(context).run(args)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Agent
|
|
5
|
+
# How long until something, rounded up, since it is still to come, in the
|
|
6
|
+
# largest unit it reaches once rounded: `45m`, `22h`, `7d`; as the console says it.
|
|
7
|
+
module DueIn
|
|
8
|
+
def self.words(seconds)
|
|
9
|
+
minutes = up(seconds, 60).clamp(1..)
|
|
10
|
+
hours = up(seconds, 3600)
|
|
11
|
+
return "#{minutes}m" if minutes < 60
|
|
12
|
+
return "#{hours}h" if hours < 24
|
|
13
|
+
|
|
14
|
+
"#{up(seconds, 86_400)}d"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.up(seconds, unit) = (seconds.to_r / unit).ceil
|
|
18
|
+
private_class_method :up
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -4,19 +4,23 @@ require "json"
|
|
|
4
4
|
require_relative "command_support"
|
|
5
5
|
require_relative "snapshots"
|
|
6
6
|
require_relative "events"
|
|
7
|
+
require_relative "job_events"
|
|
7
8
|
require_relative "waiting"
|
|
8
9
|
|
|
9
10
|
module FunCi
|
|
10
11
|
module Agent
|
|
11
12
|
# `fun-ci events [--follow] [--only failures]` (acceptance-tests.md,
|
|
12
|
-
# AT-9.16): one JSON line per event of the project's recent runs
|
|
13
|
-
# with --follow, each new one as it happens, until
|
|
13
|
+
# AT-9.16, AT-13.23): one JSON line per event of the project's recent runs
|
|
14
|
+
# and job runs, then, with --follow, each new one as it happens, until
|
|
15
|
+
# interrupted.
|
|
14
16
|
class EventsCommand
|
|
15
17
|
include CommandSupport
|
|
16
18
|
|
|
17
19
|
NAME = "events"
|
|
18
20
|
SCHEMA = 1
|
|
19
21
|
RECENT_RUNS = 10
|
|
22
|
+
# The first look: no runs, no job runs.
|
|
23
|
+
NOTHING_SEEN = [{}, {}].freeze
|
|
20
24
|
|
|
21
25
|
def initialize(context)
|
|
22
26
|
@context = context
|
|
@@ -24,7 +28,7 @@ module FunCi
|
|
|
24
28
|
|
|
25
29
|
def run(args)
|
|
26
30
|
options = Options.parse(args, takes: %i[follow only json])
|
|
27
|
-
seen = emit(
|
|
31
|
+
seen = emit(NOTHING_SEEN, options.only)
|
|
28
32
|
follow(seen, options.only) if options.follow
|
|
29
33
|
0
|
|
30
34
|
rescue Options::Invalid => e
|
|
@@ -43,13 +47,16 @@ module FunCi
|
|
|
43
47
|
# Following ends when whoever follows stops it.
|
|
44
48
|
end
|
|
45
49
|
|
|
46
|
-
# Prints what happened since `before
|
|
47
|
-
def emit(
|
|
48
|
-
after = snapshots.take(limit: RECENT_RUNS)
|
|
49
|
-
Events.between(
|
|
50
|
+
# Prints what happened since `before` ([runs, job runs]), and answers the look it took.
|
|
51
|
+
def emit((runs, jobs), only)
|
|
52
|
+
after = [snapshots.take(limit: RECENT_RUNS), snapshots.jobs(limit: RECENT_RUNS)]
|
|
53
|
+
events = Events.between(runs, after.first) + JobEvents.between(jobs, after.last)
|
|
54
|
+
events.select { |event| !only || failure?(event) }.each { |event| print(event) }
|
|
50
55
|
after
|
|
51
56
|
end
|
|
52
57
|
|
|
58
|
+
def failure?(event) = Events.failure?(event) || JobEvents.failure?(event)
|
|
59
|
+
|
|
53
60
|
def print(event)
|
|
54
61
|
@context.io.stdout.puts JSON.generate({ schema: SCHEMA, **event })
|
|
55
62
|
@context.io.stdout.flush
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../jobs/state"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Agent
|
|
7
|
+
# What happened to a project's daily and weekly job runs between two
|
|
8
|
+
# looks at them (acceptance-tests.md, AT-13.23, AT-13.28), oldest first: a
|
|
9
|
+
# job run was scheduled to start later (Jobs::Schedule), started, and
|
|
10
|
+
# ended with its state and seconds. The first look is at nothing.
|
|
11
|
+
module JobEvents
|
|
12
|
+
# state: as Jobs::State says it; seconds: how long it ran, once it ended;
|
|
13
|
+
# starts_at: when a run waiting its turn starts, nil for any other.
|
|
14
|
+
JobState = Data.define(:id, :job, :cadence, :sha, :branch, :state, :seconds, :starts_at)
|
|
15
|
+
|
|
16
|
+
# The states of a run that has not ended.
|
|
17
|
+
GOING = %w[running due scheduled].freeze
|
|
18
|
+
|
|
19
|
+
# before, after: { job run id => JobState }.
|
|
20
|
+
def self.between(before, after)
|
|
21
|
+
after.values.sort_by(&:id).flat_map { |run| of(run, before[run.id]) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.failure?(event) = event[:event] == "job_finished" && Jobs::State.needs_you?(event[:state])
|
|
25
|
+
|
|
26
|
+
def self.of(run, was)
|
|
27
|
+
about = { job: run.job, cadence: run.cadence, commit: run.sha, branch: run.branch }
|
|
28
|
+
[*begun(run, was, about), *ended(run, was, about)]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A run first seen waiting was scheduled; one that started since the
|
|
32
|
+
# last look started.
|
|
33
|
+
def self.begun(run, was, about)
|
|
34
|
+
return [{ event: "job_scheduled", **about, starts_at: run.starts_at }] if run.state == "scheduled" && !was
|
|
35
|
+
|
|
36
|
+
started?(run, was) ? [{ event: "job_started", **about }] : []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# First seen past waiting, or past it now after it waited; one
|
|
40
|
+
# cancelled while it waited never started.
|
|
41
|
+
def self.started?(run, was)
|
|
42
|
+
return false if run.state == "scheduled"
|
|
43
|
+
return true unless was
|
|
44
|
+
|
|
45
|
+
was.state == "scheduled" && run.state != "cancelled"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.ended(run, was, about)
|
|
49
|
+
return [] if was&.state == run.state || GOING.include?(run.state)
|
|
50
|
+
|
|
51
|
+
[{ event: "job_finished", **about, state: run.state, seconds: run.seconds }]
|
|
52
|
+
end
|
|
53
|
+
private_class_method :of, :begun, :started?, :ended
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Agent
|
|
5
|
+
# One job as `jobs --json` and `status --json` give it. Fields once
|
|
6
|
+
# published keep their names.
|
|
7
|
+
module JobJson
|
|
8
|
+
def self.document(report)
|
|
9
|
+
stage = report.stage
|
|
10
|
+
{ name: report.name, cadence: report.cadence, state: report.state,
|
|
11
|
+
commit: stage && { sha: report.sha, branch: report.branch }, seconds: stage&.seconds,
|
|
12
|
+
**times(report), due: report.due? }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# When its latest run started, or, while it waits its turn, is to start
|
|
16
|
+
# (Jobs::Schedule), and when it is due again.
|
|
17
|
+
def self.times(report)
|
|
18
|
+
{ started_at: iso(report.started_at), starts_at: iso(report.starts_at), due_at: iso(report.due_at) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.iso(time) = time&.utc&.iso8601
|
|
22
|
+
private_class_method :times, :iso
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "report_stage"
|
|
5
|
+
require_relative "verdict"
|
|
6
|
+
require_relative "../jobs/standings"
|
|
7
|
+
require_relative "../persistence/raw_outputs"
|
|
8
|
+
|
|
9
|
+
module FunCi
|
|
10
|
+
module Agent
|
|
11
|
+
# A daily or weekly job as an agent is told it: where it stands
|
|
12
|
+
# (Jobs::Standing), and its latest run as a RunReport::Stage named after
|
|
13
|
+
# the job, nil while it never ran, and the seconds until a run waiting its
|
|
14
|
+
# turn starts (Jobs::Schedule), nil for any other.
|
|
15
|
+
JobReport = Data.define(:standing, :stage, :starts_in)
|
|
16
|
+
|
|
17
|
+
# The jobs `status` names for a commit: those whose latest run tested it,
|
|
18
|
+
# and those failing on another commit, which an agent would not hear of otherwise.
|
|
19
|
+
CommitJobs = Data.define(:on_commit, :failing)
|
|
20
|
+
|
|
21
|
+
class CommitJobs
|
|
22
|
+
NONE = new(on_commit: [], failing: [])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class JobReport
|
|
26
|
+
VERDICTS = { "passed" => :passed, "failed" => :failed, "lost" => :failed, "over_budget" => :over_budget,
|
|
27
|
+
"running" => :undecided, "scheduled" => :undecided }.freeze
|
|
28
|
+
|
|
29
|
+
def name = standing.name
|
|
30
|
+
def cadence = standing.cadence
|
|
31
|
+
def state = standing.state
|
|
32
|
+
def needs_you? = standing.needs_you?
|
|
33
|
+
def due_at = standing.due_at
|
|
34
|
+
|
|
35
|
+
# Whether the next commit starts it.
|
|
36
|
+
def due? = standing.due_now?
|
|
37
|
+
|
|
38
|
+
# The commit and branch its latest run tested, and when that started,
|
|
39
|
+
# or, while it waits its turn, when it is to.
|
|
40
|
+
def sha = standing.run&.dig(:commit_hash)
|
|
41
|
+
def branch = standing.run&.dig(:branch)
|
|
42
|
+
def started_at = standing.run && !starts_at ? Time.parse(standing.run[:started_at]) : nil
|
|
43
|
+
def starts_at = standing.starts_at
|
|
44
|
+
|
|
45
|
+
# As `status` would exit for a stage in this state; a job that never ran, or was cancelled, has none.
|
|
46
|
+
def verdict = VERDICTS.fetch(state, :unknown)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The daily and weekly jobs of a project, as JobReports.
|
|
50
|
+
class JobReports
|
|
51
|
+
def initialize(db, project, clock)
|
|
52
|
+
@db = db
|
|
53
|
+
@project = project
|
|
54
|
+
@clock = clock
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# By name.
|
|
58
|
+
def all = Jobs::Standings.new(@db, @project, now: @clock.now).all.map { |standing| report(standing) }
|
|
59
|
+
|
|
60
|
+
# The job named, or nil when the project has no such job.
|
|
61
|
+
def named(name) = all.find { |report| report.name == name }
|
|
62
|
+
|
|
63
|
+
# The jobs whose latest run tested `sha`, and those failing on another commit.
|
|
64
|
+
def of_commit(sha)
|
|
65
|
+
reports = all
|
|
66
|
+
CommitJobs.new(on_commit: reports.select { |report| report.sha == sha },
|
|
67
|
+
failing: reports.select { |report| report.needs_you? && report.sha != sha })
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# What the job's latest run kept of its raw output, or nil.
|
|
71
|
+
def raw_output(report) = report.stage && raw.read(report.stage.id)
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def raw = Persistence::RawOutputs.for_jobs(@db.filename("main"))
|
|
76
|
+
|
|
77
|
+
def report(standing)
|
|
78
|
+
run = standing.run
|
|
79
|
+
JobReport.new(standing: standing, stage: run && stage(standing.name, run),
|
|
80
|
+
starts_in: standing.starts_at && (standing.starts_at - @clock.now))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def stage(name, run)
|
|
84
|
+
RunReport::Stage.from_row(name,
|
|
85
|
+
run.merge(budget: run[:budget] || Jobs::Job::BUDGET, raw_bytes: raw.bytes(run[:id])))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "job_report"
|
|
5
|
+
require_relative "job_why_text"
|
|
6
|
+
require_relative "job_why_json"
|
|
7
|
+
require_relative "exit_code"
|
|
8
|
+
require_relative "options"
|
|
9
|
+
|
|
10
|
+
module FunCi
|
|
11
|
+
module Agent
|
|
12
|
+
# `fun-ci why --job NAME [--json] [--raw]` (acceptance-tests.md, AT-13.21):
|
|
13
|
+
# everything kept about the job's latest run, exiting as `why` does for a
|
|
14
|
+
# stage in the same state; a job that never ran has no verdict.
|
|
15
|
+
class JobWhy
|
|
16
|
+
NO_JOBS = "this project has no daily or weekly jobs; put their scripts in .fun-ci/daily/ or .fun-ci/weekly/"
|
|
17
|
+
|
|
18
|
+
def initialize(context, jobs)
|
|
19
|
+
@context = context
|
|
20
|
+
@jobs = jobs
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Raises Options::Invalid for a name that is no job of the project.
|
|
24
|
+
def answer(options)
|
|
25
|
+
report = @jobs.named(options.job) || raise(Options::Invalid, no_job(options.job))
|
|
26
|
+
return never_ran(report, options.json) unless report.stage
|
|
27
|
+
|
|
28
|
+
options.raw ? raw(report) : say(report, options.json)
|
|
29
|
+
ExitCode::FOR.fetch(report.verdict)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def say(report, json) = json ? print_json(report) : print_lines(JobWhyText.lines(report))
|
|
35
|
+
|
|
36
|
+
def never_ran(report, json)
|
|
37
|
+
json ? print_json(report) : print_lines([JobWhyText.never_ran(report)])
|
|
38
|
+
ExitCode::FOR.fetch(:unknown)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def raw(report)
|
|
42
|
+
text = @jobs.raw_output(report)
|
|
43
|
+
return @context.io.stdout.write(text) if text
|
|
44
|
+
|
|
45
|
+
@context.io.stderr.puts "fun-ci why: no raw output is kept for job #{report.name}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def no_job(name)
|
|
49
|
+
names = @jobs.all.map(&:name)
|
|
50
|
+
return NO_JOBS if names.empty?
|
|
51
|
+
|
|
52
|
+
"no job '#{name}' in this project: its jobs are #{names.join(", ")}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def print_json(report) = @context.io.stdout.puts(JSON.generate(JobWhyJson.document(report)))
|
|
56
|
+
def print_lines(lines) = lines.each { |line| @context.io.stdout.puts line }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Agent
|
|
5
|
+
# `fun-ci why --job NAME --json` (acceptance-tests.md, AT-13.21): the job,
|
|
6
|
+
# how its latest run ended, its evidence, or else why there is none, as
|
|
7
|
+
# `why --json` gives them for a stage. Fields once published keep their
|
|
8
|
+
# names; a change bumps SCHEMA.
|
|
9
|
+
module JobWhyJson
|
|
10
|
+
SCHEMA = 1
|
|
11
|
+
|
|
12
|
+
def self.document(report)
|
|
13
|
+
stage = report.stage
|
|
14
|
+
{ schema: SCHEMA, name: report.name, cadence: report.cadence, state: report.state,
|
|
15
|
+
commit: stage && { sha: report.sha, branch: report.branch }, **ending(stage), **evidence(report),
|
|
16
|
+
raw_output: { kept: !stage&.raw_bytes.nil?, bytes: stage&.raw_bytes || 0 } }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The evidence of a run that failed or ran over budget, or else why there is none.
|
|
20
|
+
def self.evidence(report)
|
|
21
|
+
failed = report.needs_you?
|
|
22
|
+
{ evidence: failed ? report.stage.evidence.to_h : nil, no_evidence: failed ? nil : report.state }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.ending(stage)
|
|
26
|
+
{ exit_status: stage&.exit_status, signal: stage&.signal, seconds: stage&.seconds, budget: stage&.budget }
|
|
27
|
+
end
|
|
28
|
+
private_class_method :ending, :evidence
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|