fun_ci 2.0.2 → 2.1.0
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 +69 -0
- data/README.md +31 -5
- data/lib/fun_ci/agent/command_support.rb +14 -1
- data/lib/fun_ci/agent/commands.rb +1 -0
- data/lib/fun_ci/agent/context.rb +3 -2
- data/lib/fun_ci/agent/events.rb +19 -5
- data/lib/fun_ci/agent/events_command.rb +1 -1
- data/lib/fun_ci/agent/exit_code.rb +1 -1
- data/lib/fun_ci/agent/next_step.rb +26 -0
- data/lib/fun_ci/agent/options.rb +6 -4
- data/lib/fun_ci/agent/output.rb +15 -2
- data/lib/fun_ci/agent/report_stage.rb +1 -1
- data/lib/fun_ci/agent/reports.rb +13 -9
- data/lib/fun_ci/agent/run_details.rb +31 -0
- data/lib/fun_ci/agent/run_report.rb +4 -3
- data/lib/fun_ci/agent/runs_text.rb +7 -4
- data/lib/fun_ci/agent/snapshots.rb +13 -3
- data/lib/fun_ci/agent/status_command.rb +2 -2
- data/lib/fun_ci/agent/status_json.rb +2 -1
- data/lib/fun_ci/agent/status_text.rb +13 -2
- data/lib/fun_ci/agent/trunk_json.rb +32 -0
- data/lib/fun_ci/agent/trunk_reading.rb +53 -0
- data/lib/fun_ci/agent/trunk_text.rb +50 -0
- data/lib/fun_ci/agent/trunk_verdict.rb +20 -0
- data/lib/fun_ci/agent/trunk_why.rb +52 -0
- data/lib/fun_ci/agent/trunk_why_json.rb +23 -0
- data/lib/fun_ci/agent/wait_command.rb +5 -5
- data/lib/fun_ci/agent/why_command.rb +11 -0
- data/lib/fun_ci/cli.rb +1 -1
- data/lib/fun_ci/cli_help.rb +4 -2
- data/lib/fun_ci/console/board_data.rb +22 -8
- data/lib/fun_ci/console/cancelled_folding.rb +19 -0
- data/lib/fun_ci/console/console_events.rb +5 -2
- data/lib/fun_ci/console/console_log.rb +4 -1
- data/lib/fun_ci/console/console_state.rb +7 -1
- data/lib/fun_ci/console/row_order.rb +31 -0
- data/lib/fun_ci/console/run_message.rb +9 -2
- data/lib/fun_ci/console/trunk_events.rb +43 -0
- data/lib/fun_ci/console/trunk_marks.rb +53 -0
- data/lib/fun_ci/console/view.rb +9 -8
- data/lib/fun_ci/evidence/caps.rb +7 -2
- data/lib/fun_ci/persistence/active_runs.rb +5 -4
- data/lib/fun_ci/persistence/database.rb +23 -2
- data/lib/fun_ci/persistence/null_recorder.rb +8 -0
- data/lib/fun_ci/persistence/pipeline_recorder.rb +3 -0
- data/lib/fun_ci/persistence/pipeline_run.rb +24 -3
- data/lib/fun_ci/persistence/trunk_checks.rb +68 -0
- data/lib/fun_ci/persistence/trunk_fetches.rb +57 -0
- data/lib/fun_ci/persistence/trunk_recording.rb +49 -0
- data/lib/fun_ci/pipeline/pipeline_forker.rb +17 -2
- data/lib/fun_ci/pipeline/process_runner.rb +18 -4
- data/lib/fun_ci/pipeline/trigger.rb +10 -1
- data/lib/fun_ci/pipeline/trigger_command.rb +9 -5
- data/lib/fun_ci/pipeline/trigger_params.rb +5 -3
- data/lib/fun_ci/pipeline/trunk_run.rb +81 -0
- data/lib/fun_ci/setup/agent_instructions.rb +3 -0
- data/lib/fun_ci/setup/project_config.rb +3 -1
- data/lib/fun_ci/setup/settings.rb +35 -3
- data/lib/fun_ci/setup/setup_checker.rb +7 -2
- data/lib/fun_ci/setup/trunk_report.rb +28 -0
- data/lib/fun_ci/trunk/check.rb +56 -0
- data/lib/fun_ci/trunk/checker.rb +91 -0
- data/lib/fun_ci/trunk/description.rb +52 -0
- data/lib/fun_ci/trunk/fetch.rb +58 -0
- data/lib/fun_ci/trunk/first_fetch.rb +27 -0
- data/lib/fun_ci/trunk/git.rb +102 -0
- data/lib/fun_ci/trunk/local.rb +58 -0
- data/lib/fun_ci/trunk/merge_check.rb +54 -0
- data/lib/fun_ci/trunk/regions.rb +38 -0
- data/lib/fun_ci/trunk/resolver.rb +64 -0
- data/lib/fun_ci/trunk/shown.rb +47 -0
- data/lib/fun_ci/trunk/tip_reader.rb +39 -0
- data/lib/fun_ci/version.rb +1 -1
- metadata +30 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5ea6a4ee9221da8020682980de5e05ad481540f66f9b187620ce121e16ec399
|
|
4
|
+
data.tar.gz: 27d73d17a2a6089d219f5bceaabe1a39385392c25d6e7ec095862eb84d637135
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eebf4b0852e2ecd06abcee7c8a5ce1b72d29705767b55771b349b4882f1f0d0655d5f96eb49ad6b11eee17216b5d5fa7f6f7db35c9f6da69a81d5151987f8d92
|
|
7
|
+
data.tar.gz: 3a3d674d245e9cd4fae2ecd6afbd39a645e36f06ea305f9454cd4676462da3128903cd559252b500cb187a12d398a8fe6b654d2c90df192680d5ed2ef6788f03
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.1.0] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Each run checks whether its commit would merge cleanly with the trunk:
|
|
14
|
+
`trunk:` in `.fun-ci/config`, or else the remote's default branch.
|
|
15
|
+
`status` and `wait` print where the commit stands, with its age; for a
|
|
16
|
+
conflict, the files and when and how to integrate. A conflict is not a
|
|
17
|
+
failure and changes no exit code, unless you ask with `--trunk`, which exits
|
|
18
|
+
6 for a run that passed but conflicts and waits for the check.
|
|
19
|
+
`fun-ci why REV trunk` prints each conflicted region. `status --json`,
|
|
20
|
+
`runs` and `events` carry the check; `fun-ci check` names the trunk.
|
|
21
|
+
- fun-ci fetches the trunk itself, at most every five minutes, into
|
|
22
|
+
`refs/fun-ci/trunk/`, touching none of your refs, and says so the first time.
|
|
23
|
+
`trunk_fetch: false` stops it, and `trunk: none` turns the check off.
|
|
24
|
+
- The console says when a branch conflicts with the trunk, `conflicts with
|
|
25
|
+
main` under it, and plays a knot tying, and later untying, in the header.
|
|
26
|
+
This needs renderer 2.1.0, which the platform gems bring; with an older
|
|
27
|
+
renderer, as `cargo install` left it, the console works but shows no
|
|
28
|
+
conflicts.
|
|
29
|
+
- `fun-ci --help` lists `--raw`, which it left out.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- The console's table is new. Each branch has one row, its newest run's,
|
|
33
|
+
under its project's name, and reads as one phrase: the branch, a mark each
|
|
34
|
+
for lint, the build, the fast suite and the slow suite, joined into a track
|
|
35
|
+
that fills from left to right (`✓─✓─◆┄·`), what happened (`the fast suite
|
|
36
|
+
failed after 1.4s`, `running the fast and slow suites · 7s`) and when. A
|
|
37
|
+
legend at the top of the table names each stage and says what it is for,
|
|
38
|
+
with a line from each name down through every project to its column of
|
|
39
|
+
marks; when there is no room for it, one line above the keys names the
|
|
40
|
+
marks. What needs you, a failure, a timeout or a conflict, comes first, with
|
|
41
|
+
a stripe in the colour of why, and the first of it, or the row under the
|
|
42
|
+
cursor, sits in one card that breathes: wine when its row needs you, indigo
|
|
43
|
+
when the cursor only rests there. Passed rows are pale, and fold into one
|
|
44
|
+
line per project when nothing needs you. The header's night sky carries on
|
|
45
|
+
below the table, and the keys sit on caps. On a wide terminal the table
|
|
46
|
+
widens a little and sits in the middle; on a short one it folds and closes
|
|
47
|
+
up before it leaves anything out, and says how many passed rows it left out.
|
|
48
|
+
`c cancel` shows only when there is something to cancel.
|
|
49
|
+
- A stage's mark lights up and fades back when something happens to it. A
|
|
50
|
+
pass glows gold and eases back to the mark's own colour, a timeout pulses
|
|
51
|
+
amber twice, and a failure flares white on red and cools into the row. They
|
|
52
|
+
last as long however often the console redraws; they used to run faster
|
|
53
|
+
when boards arrived quickly.
|
|
54
|
+
- A run that ends washes its row in teal, coral or amber, draining from left
|
|
55
|
+
to right, and a band of light travels along each running row.
|
|
56
|
+
- The failure and pass banners keep the sky behind them, take their colours
|
|
57
|
+
from the table's, and name the stage in full (`>>> THE FAST SUITE FAILED
|
|
58
|
+
<<<`).
|
|
59
|
+
|
|
60
|
+
### Deprecated
|
|
61
|
+
- `fun-ci trigger --no-validate`, the old name for `--background`, now goes
|
|
62
|
+
in 3.0 rather than 2.1 as 2.0 said: removing a flag in a minor release
|
|
63
|
+
would break the scripts and hooks that still pass it. It still works, and
|
|
64
|
+
says so on stderr.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- A console row wider than the terminal, from a long branch name or a
|
|
68
|
+
failure word, wrapped onto the next line and pushed the board up, hiding
|
|
69
|
+
the newest run under the header. Rows are now cut to fit, the branch first,
|
|
70
|
+
and never wrap; the footer too.
|
|
71
|
+
- A branch named in Chinese, Japanese or Korean fills two columns a
|
|
72
|
+
character, but the console counted one, so its marks slid out of line with
|
|
73
|
+
the other rows' and a long name ran into them. Names are now measured, and
|
|
74
|
+
cut, by the columns they fill.
|
|
75
|
+
- `.fun-ci/console.log`, where the console notes what went wrong between it
|
|
76
|
+
and the renderer, was left for `git add` to sweep into a commit. The console
|
|
77
|
+
now writes a `.fun-ci/.gitignore` naming it, unless the project has one there.
|
|
78
|
+
|
|
10
79
|
## [2.0.2] - 2026-09-29
|
|
11
80
|
|
|
12
81
|
### Fixed
|
data/README.md
CHANGED
|
@@ -59,7 +59,9 @@ Results are kept in SQLite under `$XDG_STATE_HOME/fun-ci/` (`~/.local/state/fun-
|
|
|
59
59
|
fun-ci console
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
The console
|
|
62
|
+
The console shows where each branch stands, one row per branch under its project's name: a mark for each stage (lint, the build, the fast suite and the slow suite, named in a legend at the top of the table), what happened (`the fast suite failed after 1.4s`) and when. What needs you comes first, in one deep block; passed rows stay pale. Above them is a picture that tells you what is happening without reading a word.
|
|
63
|
+
|
|
64
|
+

|
|
63
65
|
|
|
64
66
|
While a run runs, a rocket flies. Each milestone a run passes has its own set of scenes, so you can tell which one it was from across the room, and a failure has its own.
|
|
65
67
|
|
|
@@ -86,10 +88,12 @@ Five minutes after the latest run finished, the header goes quiet: a starry nigh
|
|
|
86
88
|
|---|---|
|
|
87
89
|
|  |  |
|
|
88
90
|
|
|
91
|
+
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
|
+
|
|
89
93
|
Keys:
|
|
90
94
|
|
|
91
95
|
- `j` and `k`, or the arrow keys, move the cursor down and up
|
|
92
|
-
- `c` cancels the run under the cursor:
|
|
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
|
|
93
97
|
- `q` quits
|
|
94
98
|
|
|
95
99
|
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 come from the renderer's headless mode, drawn with its own bitmap font; in your terminal the text is in your terminal's font.
|
|
@@ -116,6 +120,7 @@ The section `fun-ci init` writes into `AGENTS.md` tells an agent to run that com
|
|
|
116
120
|
| 3 | Still undecided (`wait --within 30s` gives up at your deadline) |
|
|
117
121
|
| 4 | Superseded by a newer commit on the branch (`wait --follow-branch` moves on to it) |
|
|
118
122
|
| 5 | No run for the commit |
|
|
123
|
+
| 6 | Passed, but conflicts with the trunk (only with `--trunk`) |
|
|
119
124
|
| 64 | Usage error |
|
|
120
125
|
|
|
121
126
|
A failed stage comes with its evidence. `status` and `wait` end with a digest and the command that shows the rest:
|
|
@@ -161,6 +166,27 @@ The whole output: fun-ci why 2330979 fast --raw
|
|
|
161
166
|
|
|
162
167
|
`fun-ci runs` lists recent runs one line each, and `fun-ci events --follow` prints each run's events as JSON lines as they happen, for a supervising agent or a status bar.
|
|
163
168
|
|
|
169
|
+
### Where your commit stands against the trunk
|
|
170
|
+
|
|
171
|
+
Each run also checks whether its commit would merge cleanly with your trunk: `trunk:` in `.fun-ci/config` if you name one, otherwise your remote's default branch. `status` and `wait` print where it stands, and for a conflict, the files and what to do about it:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
$ fun-ci status
|
|
175
|
+
fun-ci: f473fdb "Add shipping to the cart total" on feat/cart
|
|
176
|
+
lint passed 0.2s
|
|
177
|
+
build passed 0.3s
|
|
178
|
+
fast passed 0.1s
|
|
179
|
+
slow passed 0.2s (not needed)
|
|
180
|
+
trunk conflicts origin/main 0e15acf, fetched just now, 1 ahead, 1 behind
|
|
181
|
+
lib/cart.rb
|
|
182
|
+
Conflicts with origin/main in 1 file. When the task is done, integrate: git pull origin main (or git pull --rebase origin main if the branch isn't shared)
|
|
183
|
+
fun-ci why f473fdb trunk
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
A conflict is not a failure, so that run still exits 0. An agent that should integrate before calling its work done asks with `--trunk`, and gets 6 for a run that passed but conflicts; `fun-ci init` tells agents to. `fun-ci why f473fdb trunk` merges again and prints each conflicted region with the lines around it.
|
|
187
|
+
|
|
188
|
+
fun-ci fetches the trunk itself, at most every five minutes, into a ref of its own (`refs/fun-ci/trunk/...`), so your `origin/main` and `git status` are left as they were. The first fetch says so. `trunk_fetch: false` in `.fun-ci/config` stops it, `trunk_fetch: 30m` spaces it out, and `trunk: none` turns the check off. A trunk fetched over an hour ago, or whose last fetch failed, is marked STALE wherever it is shown.
|
|
189
|
+
|
|
164
190
|
### What fun-ci keeps when a stage fails
|
|
165
191
|
|
|
166
192
|
With no configuration, fun-ci runs the presets for the tools your project has when a failure shows them. There are presets for the test runners, compilers, linters and type checkers of every stack `fun-ci init` knows, and for JSON logs from logstash-logback-encoder, ECS, pino and structlog; [docs/stacks.md](docs/stacks.md#presets) lists each one with what it picks out and when it runs. Each preset is checked against the recorded output of a real failing run of its tool. `fun-ci check` lists the ones that apply to your project.
|
|
@@ -197,9 +223,9 @@ fun-ci install-hooks Install post-commit and pre-push
|
|
|
197
223
|
fun-ci install-hooks post-commit Install a single hook type
|
|
198
224
|
fun-ci check Verify .fun-ci/ setup
|
|
199
225
|
fun-ci console Watch the runs
|
|
200
|
-
fun-ci status [commit]
|
|
226
|
+
fun-ci status [commit] [--trunk] Where a commit's run stands; the verdict is the exit code
|
|
201
227
|
fun-ci wait [commit] Wait until that verdict is decided, then exit with it
|
|
202
|
-
fun-ci why [commit] [stage]
|
|
228
|
+
fun-ci why [commit] [stage|trunk] Everything kept about why a stage failed, or the conflict with the trunk
|
|
203
229
|
fun-ci runs This project's recent runs, newest first
|
|
204
230
|
fun-ci events [--follow] The runs' events as JSON lines
|
|
205
231
|
fun-ci extract <stage> --output <file> Try a stage's extractors on a saved output
|
|
@@ -215,7 +241,7 @@ fun-ci prune Remove fun-ci's worktrees when n
|
|
|
215
241
|
- Run `fun-ci install-hooks` again. The background pipeline now runs after the commit (`post-commit`), so it tests the commit you just made; the `pre-commit` hook tested the one before it. Until you do, each run tests the commit before yours, and `fun-ci check` warns you. Installing removes the `pre-commit` hook fun-ci 1.x wrote and leaves a hook of your own alone (`fun-ci check` warns if yours still calls fun-ci). The new `pre-push` hook waits for the verdict instead of running the pipeline again.
|
|
216
242
|
- The stages run in a worktree at the commit, not in your checkout. A script that relied on uncommitted files in your checkout won't see them any more.
|
|
217
243
|
- The database moved from `$TMPDIR` to `$XDG_STATE_HOME/fun-ci/`. Runs recorded by 1.x are not carried over.
|
|
218
|
-
- `fun-ci-trigger` and `fun-ci-tui` are gone; use `fun-ci trigger` and `fun-ci console`. `fun-ci trigger --no-validate` still works until
|
|
244
|
+
- `fun-ci-trigger` and `fun-ci-tui` are gone; use `fun-ci trigger` and `fun-ci console`. `fun-ci trigger --no-validate` still works until 3.0, as the old name for `--background`.
|
|
219
245
|
- The console needs `fun-ci-renderer`, which the platform gems include (see [Watching: the console](#watching-the-console)).
|
|
220
246
|
|
|
221
247
|
The whole list is in [CHANGELOG.md](CHANGELOG.md).
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "options"
|
|
4
4
|
require_relative "reports"
|
|
5
5
|
require_relative "exit_code"
|
|
6
|
+
require_relative "trunk_verdict"
|
|
6
7
|
|
|
7
8
|
module FunCi
|
|
8
9
|
module Agent
|
|
@@ -16,7 +17,19 @@ module FunCi
|
|
|
16
17
|
@context.git.resolve(rev) || raise(Options::Invalid, "git can't find the commit '#{rev}'")
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
# The commit's newest run, judged on the trunk too when the agent asked
|
|
21
|
+
# (--trunk), checked again first when the trunk has moved since.
|
|
22
|
+
def report_for(sha, options)
|
|
23
|
+
report = reports.for(sha, options.need)
|
|
24
|
+
return report unless options.trunk && report
|
|
25
|
+
|
|
26
|
+
report = reports.recheck_trunk(sha).then { reports.for(sha, options.need) } if report.trunk&.moved_to
|
|
27
|
+
report.with(verdict: TrunkVerdict.of(report.verdict, report.trunk))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def output(options) = Output.new(@context.io.stdout, json: options.json, trunk: options.trunk)
|
|
31
|
+
|
|
32
|
+
def reports = @reports ||= Reports.new(@context.db, @context.git, @context.clock, @context.trunk)
|
|
20
33
|
|
|
21
34
|
def usage(message)
|
|
22
35
|
@context.io.stderr.puts "fun-ci #{self.class::NAME}: #{message}"
|
data/lib/fun_ci/agent/context.rb
CHANGED
|
@@ -4,7 +4,8 @@ module FunCi
|
|
|
4
4
|
module Agent
|
|
5
5
|
# What every agent command answers from: the database, the project's git,
|
|
6
6
|
# where to print, the time (#now, #pause) and the pipeline (#start a run,
|
|
7
|
-
# #watch the database for slow suites that died)
|
|
8
|
-
|
|
7
|
+
# #watch the database for slow suites that died) and the trunk as the
|
|
8
|
+
# project's git has it now (#now_at(tip), #check(sha, fetches)).
|
|
9
|
+
Context = Data.define(:db, :git, :io, :clock, :pipeline, :trunk)
|
|
9
10
|
end
|
|
10
11
|
end
|
data/lib/fun_ci/agent/events.rb
CHANGED
|
@@ -5,10 +5,16 @@ module FunCi
|
|
|
5
5
|
# What happened to a project's runs between two looks at them
|
|
6
6
|
# (acceptance-tests.md, AT-9.16), oldest run first: a run started, each
|
|
7
7
|
# stage that finished in the order it finished, a run that passed or
|
|
8
|
-
# failed, a run a newer commit superseded
|
|
8
|
+
# failed, a run a newer commit superseded, a check of its commit against
|
|
9
|
+
# the trunk recorded or made again. The first look is at nothing.
|
|
9
10
|
module Events
|
|
10
|
-
# finished: [{ stage:, state:, seconds: }, ...] in the order they finished
|
|
11
|
-
|
|
11
|
+
# finished: [{ stage:, state:, seconds: }, ...] in the order they finished;
|
|
12
|
+
# trunk: the latest check of its commit, as status --json gives it, or nil.
|
|
13
|
+
RunState = Data.define(:id, :sha, :branch, :status, :finished, :superseded_by, :trunk)
|
|
14
|
+
|
|
15
|
+
class RunState
|
|
16
|
+
def initialize(trunk: nil, **) = super
|
|
17
|
+
end
|
|
12
18
|
OUTCOMES = { "completed" => "passed", "failed" => "failed" }.freeze
|
|
13
19
|
FAILED_STATES = %w[failed over_budget].freeze
|
|
14
20
|
|
|
@@ -27,7 +33,7 @@ module FunCi
|
|
|
27
33
|
about = { commit: run.sha, branch: run.branch }
|
|
28
34
|
[*(was ? [] : [{ event: "run_started", **about }]),
|
|
29
35
|
*newly_finished(run, was).map { |stage| { event: "stage_finished", **about, **stage } },
|
|
30
|
-
*ended(run, was, about)]
|
|
36
|
+
*checked(run, was, about), *ended(run, was, about)]
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
def self.newly_finished(run, was)
|
|
@@ -35,13 +41,21 @@ module FunCi
|
|
|
35
41
|
run.finished.reject { |stage| seen.include?(stage[:stage]) }
|
|
36
42
|
end
|
|
37
43
|
|
|
44
|
+
# A check is new when its state or its trunk tip is.
|
|
45
|
+
def self.checked(run, was, about)
|
|
46
|
+
trunk = run.trunk
|
|
47
|
+
return [] if trunk.nil? || trunk.values_at(:state, :sha) == was&.trunk&.values_at(:state, :sha)
|
|
48
|
+
|
|
49
|
+
[{ event: "trunk_checked", **about, trunk: trunk }]
|
|
50
|
+
end
|
|
51
|
+
|
|
38
52
|
def self.ended(run, was, about)
|
|
39
53
|
return [] if was&.status == run.status
|
|
40
54
|
return [{ event: "run_superseded", **about, by_commit: run.superseded_by }] if run.status == "cancelled"
|
|
41
55
|
|
|
42
56
|
OUTCOMES.key?(run.status) ? [{ event: "run_finished", **about, state: OUTCOMES.fetch(run.status) }] : []
|
|
43
57
|
end
|
|
44
|
-
private_class_method :of, :newly_finished, :ended
|
|
58
|
+
private_class_method :of, :newly_finished, :checked, :ended
|
|
45
59
|
end
|
|
46
60
|
end
|
|
47
61
|
end
|
|
@@ -4,7 +4,7 @@ module FunCi
|
|
|
4
4
|
module Agent
|
|
5
5
|
# What `status` and `wait` exit with (acceptance-tests.md, §9).
|
|
6
6
|
module ExitCode
|
|
7
|
-
FOR = { passed: 0, failed: 1, over_budget: 2, undecided: 3, superseded: 4, unknown: 5 }.freeze
|
|
7
|
+
FOR = { passed: 0, failed: 1, over_budget: 2, undecided: 3, superseded: 4, unknown: 5, conflicts: 6 }.freeze
|
|
8
8
|
USAGE = 64
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Agent
|
|
5
|
+
# What to do about a conflict with the trunk, built from the trunk that was
|
|
6
|
+
# resolved: a pull that fetches first, so the trunk integrated is never
|
|
7
|
+
# older than the one checked. A rebase of a pushed branch needs a force
|
|
8
|
+
# push, so off the trunk branch a merge comes first.
|
|
9
|
+
module NextStep
|
|
10
|
+
def self.line(tip, files, branch:)
|
|
11
|
+
count = files.size == 1 ? "1 file" : "#{files.size} files"
|
|
12
|
+
"Conflicts with #{tip.ref} in #{count}. When the task is done, integrate: #{command(tip, branch)}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.command(tip, branch)
|
|
16
|
+
return "git merge #{tip.branch}" unless tip.remote
|
|
17
|
+
|
|
18
|
+
pull = "#{tip.remote} #{tip.branch}"
|
|
19
|
+
return "git pull --rebase #{pull}" if branch == tip.branch
|
|
20
|
+
|
|
21
|
+
"git pull #{pull} (or git pull --rebase #{pull} if the branch isn't shared)"
|
|
22
|
+
end
|
|
23
|
+
private_class_method :command
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/fun_ci/agent/options.rb
CHANGED
|
@@ -11,16 +11,18 @@ module FunCi
|
|
|
11
11
|
module Options
|
|
12
12
|
class Invalid < StandardError; end
|
|
13
13
|
|
|
14
|
-
Parsed = Data.define(:rev, :stage, :need, :json, :within, :follow_branch, :limit, :branch, :follow, :only, :raw
|
|
14
|
+
Parsed = Data.define(:rev, :stage, :need, :json, :within, :follow_branch, :limit, :branch, :follow, :only, :raw,
|
|
15
|
+
:trunk)
|
|
15
16
|
DEFAULTS = { need: "fast", json: false, within: nil, follow_branch: false, limit: 10, branch: nil,
|
|
16
|
-
follow: false, only: nil, raw: false }.freeze
|
|
17
|
+
follow: false, only: nil, raw: false, trunk: false }.freeze
|
|
17
18
|
SWITCHES = { need: ["--need LEVEL"], json: ["--json"], within: ["--within DURATION"], raw: ["--raw"],
|
|
18
19
|
follow_branch: ["--follow-branch"], limit: ["-n N", Integer], branch: ["--branch NAME"],
|
|
19
|
-
follow: ["--follow"], only: ["--only KIND"] }.freeze
|
|
20
|
+
follow: ["--follow"], only: ["--only KIND"], trunk: ["--trunk"] }.freeze
|
|
20
21
|
FILTERS = %w[failures].freeze
|
|
21
22
|
MULTIPLIERS = { "" => 1, "s" => 1, "m" => 60 }.freeze
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
# What `why` explains: a stage, or the run's conflict with the trunk.
|
|
25
|
+
STAGES = %w[lint build fast slow trunk].freeze
|
|
24
26
|
|
|
25
27
|
def self.parse(args, takes:)
|
|
26
28
|
values = DEFAULTS.dup
|
data/lib/fun_ci/agent/output.rb
CHANGED
|
@@ -7,20 +7,25 @@ require_relative "why_json"
|
|
|
7
7
|
require_relative "status_json"
|
|
8
8
|
require_relative "runs_text"
|
|
9
9
|
require_relative "exit_code"
|
|
10
|
+
require_relative "evidence_text"
|
|
11
|
+
require_relative "trunk_why_json"
|
|
10
12
|
|
|
11
13
|
module FunCi
|
|
12
14
|
module Agent
|
|
13
15
|
# Prints what an agent command found, as text or, with --json, as JSON.
|
|
14
16
|
class Output
|
|
17
|
+
NO_CONFLICT = "No conflict with the trunk to explain."
|
|
15
18
|
NO_STAGE = "No stage it needs failed or ran over budget; name one to see it: fun-ci why REV STAGE"
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
# trunk: whether the agent asked about the trunk (--trunk).
|
|
21
|
+
def initialize(stdout, json:, trunk: false)
|
|
18
22
|
@stdout = stdout
|
|
19
23
|
@json = json
|
|
24
|
+
@trunk = trunk
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
def report(report)
|
|
23
|
-
@json ? print_json(StatusJson.document(report)) : print_lines(StatusText.lines(report))
|
|
28
|
+
@json ? print_json(StatusJson.document(report)) : print_lines(StatusText.lines(report, trunk: @trunk))
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
# Everything kept about the stage named, or says no stage decided the verdict.
|
|
@@ -32,6 +37,14 @@ module FunCi
|
|
|
32
37
|
print_lines(WhyText.lines(report, stage))
|
|
33
38
|
end
|
|
34
39
|
|
|
40
|
+
# The run's conflict with the trunk; document nil when there is none to explain.
|
|
41
|
+
def why_trunk(report, document)
|
|
42
|
+
return print_json(TrunkWhyJson.document(report, document)) if @json
|
|
43
|
+
|
|
44
|
+
print_lines([StatusText.header(report), *StatusText.trunk(report, true),
|
|
45
|
+
*(document ? EvidenceText.lines(document) : ["", NO_CONFLICT])])
|
|
46
|
+
end
|
|
47
|
+
|
|
35
48
|
# Says the commit has no run, and answers the exit code for that.
|
|
36
49
|
def unknown(sha)
|
|
37
50
|
@json ? print_json(StatusJson.unknown(sha)) : print_lines(["fun-ci: no run for #{sha[0, 7]} in this project."])
|
|
@@ -6,7 +6,7 @@ require_relative "../persistence/stage_job"
|
|
|
6
6
|
|
|
7
7
|
module FunCi
|
|
8
8
|
module Agent
|
|
9
|
-
RunReport = Data.define(:sha, :subject, :branch, :need, :stages, :verdict, :deciding, :superseded_by)
|
|
9
|
+
RunReport = Data.define(:sha, :subject, :branch, :need, :stages, :verdict, :deciding, :superseded_by, :trunk)
|
|
10
10
|
|
|
11
11
|
# One stage of a run, as an agent is told it.
|
|
12
12
|
class RunReport
|
data/lib/fun_ci/agent/reports.rb
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../persistence/project_runs"
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "../persistence/raw_outputs"
|
|
4
|
+
require_relative "run_details"
|
|
6
5
|
require_relative "run_report"
|
|
7
6
|
|
|
8
7
|
module FunCi
|
|
9
8
|
module Agent
|
|
10
9
|
# The runs of the project git names, as an agent is told them.
|
|
11
10
|
class Reports
|
|
12
|
-
|
|
11
|
+
# trunk: where the project's trunk is now (#now_at(tip)), and checks against it (#check(sha, fetches)).
|
|
12
|
+
def initialize(db, git, clock, trunk)
|
|
13
13
|
@db = db
|
|
14
14
|
@git = git
|
|
15
|
-
|
|
16
|
-
@
|
|
15
|
+
project = git.toplevel
|
|
16
|
+
@runs = Persistence::ProjectRuns.new(db, project)
|
|
17
|
+
@details = RunDetails.new(db, TrunkReading.new(db, project, clock, trunk))
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
# What the stage kept of its raw output, or nil.
|
|
20
|
-
def raw_output(stage) =
|
|
21
|
+
def raw_output(stage) = @details.raw_output(stage)
|
|
21
22
|
|
|
22
23
|
# The commit's newest run, or nil.
|
|
23
24
|
def for(sha, need)
|
|
@@ -25,6 +26,9 @@ module FunCi
|
|
|
25
26
|
run && of(run, need)
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
# Checks the commit against where the trunk is now, and keeps the check.
|
|
30
|
+
def recheck_trunk(sha) = @details.recheck_trunk(sha)
|
|
31
|
+
|
|
28
32
|
# Notes that an agent is waiting on the commit's newest run, as of `at`.
|
|
29
33
|
def mark_waited(sha, at)
|
|
30
34
|
run = @runs.latest_of_commit(sha)
|
|
@@ -38,9 +42,9 @@ module FunCi
|
|
|
38
42
|
|
|
39
43
|
def of(run, need)
|
|
40
44
|
superseded_by = run[:status] == "cancelled" ? @runs.superseded_by(run) : nil
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
RunReport.build(run: run, jobs: @details.jobs(run), need: need,
|
|
46
|
+
commit: { subject: @git.subject(run[:commit_hash]), superseded_by: superseded_by,
|
|
47
|
+
trunk: @details.trunk(run) })
|
|
44
48
|
end
|
|
45
49
|
end
|
|
46
50
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../persistence/stage_job"
|
|
4
|
+
require_relative "../persistence/raw_outputs"
|
|
5
|
+
require_relative "trunk_reading"
|
|
6
|
+
|
|
7
|
+
module FunCi
|
|
8
|
+
module Agent
|
|
9
|
+
# What is recorded about one of a project's runs beyond its row: its
|
|
10
|
+
# stages, the raw output they kept, and its commit's check against the trunk.
|
|
11
|
+
class RunDetails
|
|
12
|
+
# trunk: reads the run's trunk (a TrunkReading).
|
|
13
|
+
def initialize(db, trunk)
|
|
14
|
+
@db = db
|
|
15
|
+
@raw = Persistence::RawOutputs.beside(db.filename("main"))
|
|
16
|
+
@trunk = trunk
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def jobs(run)
|
|
20
|
+
Persistence::StageJob.for_run(@db, run[:id]).map { |job| job.merge(raw_bytes: @raw.bytes(job[:id])) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# What the stage kept of its raw output, or nil.
|
|
24
|
+
def raw_output(stage) = stage.id && @raw.read(stage.id)
|
|
25
|
+
|
|
26
|
+
# How the run's commit stands against the trunk, or nil for a run that began no check.
|
|
27
|
+
def trunk(run) = @trunk.for(run)
|
|
28
|
+
def recheck_trunk(sha) = @trunk.recheck(sha)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -10,10 +10,11 @@ module FunCi
|
|
|
10
10
|
class RunReport
|
|
11
11
|
STAGES = %w[lint build fast slow].freeze
|
|
12
12
|
|
|
13
|
-
# deciding: the stage whose failure or overrun decided the verdict, if one did
|
|
14
|
-
|
|
13
|
+
# deciding: the stage whose failure or overrun decided the verdict, if one did;
|
|
14
|
+
# trunk: how its commit stands against the trunk (a Trunk::Shown), if it was checked.
|
|
15
|
+
def initialize(deciding: nil, trunk: nil, **) = super
|
|
15
16
|
|
|
16
|
-
# commit: the run's subject
|
|
17
|
+
# commit: the run's subject, the commit that superseded it, if any, and its trunk check.
|
|
17
18
|
def self.build(run:, jobs:, need:, commit:)
|
|
18
19
|
verdict = Verdict.decide(run_status: run[:status], stages: jobs, need: need)
|
|
19
20
|
new(sha: run[:commit_hash], branch: run[:branch], need: need, stages: stages(jobs), verdict: verdict,
|
|
@@ -5,7 +5,8 @@ require_relative "run_report"
|
|
|
5
5
|
module FunCi
|
|
6
6
|
module Agent
|
|
7
7
|
# One line per run, the columns lined up: short SHA, branch, age, each
|
|
8
|
-
# stage's outcome in a word of four letters at most,
|
|
8
|
+
# stage's outcome in a word of four letters at most, a conflict with the
|
|
9
|
+
# trunk, as of the run's check, and the subject.
|
|
9
10
|
module RunsText
|
|
10
11
|
WORDS = { "passed" => "ok", "failed" => "FAIL", "over_budget" => "OVER", "running" => "...", "waiting" => "-",
|
|
11
12
|
"cancelled" => "x" }.freeze
|
|
@@ -18,10 +19,12 @@ module FunCi
|
|
|
18
19
|
|
|
19
20
|
def self.line(report, age, (branch_width, age_width))
|
|
20
21
|
stages = report.stages.map { |stage| "#{stage.name} #{WORDS.fetch(stage.state).ljust(4)}" }.join(" ")
|
|
21
|
-
"#{report.sha[0,
|
|
22
|
-
|
|
22
|
+
"#{report.sha[0, 7]} #{report.branch.ljust(branch_width)} #{age.ljust(age_width)} #{stages} " \
|
|
23
|
+
"#{conflict(report.trunk)}#{report.subject}"
|
|
23
24
|
end
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
def self.conflict(trunk) = trunk&.state == "conflicts" ? "conflicts #{trunk.check.tip.ref} " : ""
|
|
27
|
+
private_class_method :line, :conflict
|
|
25
28
|
end
|
|
26
29
|
end
|
|
27
30
|
end
|
|
@@ -4,15 +4,19 @@ require_relative "../persistence/project_runs"
|
|
|
4
4
|
require_relative "../persistence/stage_job"
|
|
5
5
|
require_relative "run_report"
|
|
6
6
|
require_relative "events"
|
|
7
|
+
require_relative "trunk_reading"
|
|
8
|
+
require_relative "trunk_json"
|
|
7
9
|
|
|
8
10
|
module FunCi
|
|
9
11
|
module Agent
|
|
10
12
|
# Looks at the newest runs of the project git names, as `events` compares
|
|
11
13
|
# them: { run id => Events::RunState }.
|
|
12
14
|
class Snapshots
|
|
13
|
-
def initialize(db, git)
|
|
15
|
+
def initialize(db, git, clock)
|
|
16
|
+
project = git.toplevel
|
|
14
17
|
@db = db
|
|
15
|
-
@runs = Persistence::ProjectRuns.new(db,
|
|
18
|
+
@runs = Persistence::ProjectRuns.new(db, project)
|
|
19
|
+
@trunk = TrunkReading.new(db, project, clock, TrunkReading::UNREAD)
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def take(limit:)
|
|
@@ -24,7 +28,13 @@ module FunCi
|
|
|
24
28
|
def state_of(run)
|
|
25
29
|
superseded_by = run[:status] == "cancelled" ? @runs.superseded_by(run) : nil
|
|
26
30
|
Events::RunState.new(id: run[:id], sha: run[:commit_hash], branch: run[:branch], status: run[:status],
|
|
27
|
-
finished: finished(run), superseded_by: superseded_by)
|
|
31
|
+
finished: finished(run), superseded_by: superseded_by, trunk: trunk(run))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The latest check recorded; a check still going has none yet.
|
|
35
|
+
def trunk(run)
|
|
36
|
+
shown = @trunk.for(run)
|
|
37
|
+
shown&.check ? TrunkJson.document(shown) : nil
|
|
28
38
|
end
|
|
29
39
|
|
|
30
40
|
def finished(run)
|
|
@@ -16,9 +16,9 @@ module FunCi
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def run(args)
|
|
19
|
-
options = Options.parse(args, takes: %i[need json])
|
|
19
|
+
options = Options.parse(args, takes: %i[need json trunk])
|
|
20
20
|
sha = resolve(options.rev)
|
|
21
|
-
answer(sha,
|
|
21
|
+
answer(sha, report_for(sha, options), output(options))
|
|
22
22
|
rescue Options::Invalid => e
|
|
23
23
|
usage(e.message)
|
|
24
24
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "run_report"
|
|
4
|
+
require_relative "trunk_json"
|
|
4
5
|
|
|
5
6
|
module FunCi
|
|
6
7
|
module Agent
|
|
@@ -12,7 +13,7 @@ module FunCi
|
|
|
12
13
|
def self.document(report)
|
|
13
14
|
{ schema: SCHEMA, commit: { sha: report.sha, branch: report.branch, subject: report.subject },
|
|
14
15
|
need: report.need, verdict: report.verdict.to_s, stages: report.stages.map { |stage| stage(stage) },
|
|
15
|
-
superseded_by: report.superseded_by }
|
|
16
|
+
superseded_by: report.superseded_by, trunk: report.trunk && TrunkJson.document(report.trunk) }
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def self.unknown(sha) = { schema: SCHEMA, commit: { sha: sha }, verdict: "unknown" }
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "run_report"
|
|
4
4
|
require_relative "verdict"
|
|
5
5
|
require_relative "digest"
|
|
6
|
+
require_relative "trunk_text"
|
|
6
7
|
|
|
7
8
|
module FunCi
|
|
8
9
|
module Agent
|
|
@@ -11,10 +12,19 @@ module FunCi
|
|
|
11
12
|
module StatusText
|
|
12
13
|
WORDS = { "failed" => "FAILED", "over_budget" => "OVER BUDGET" }.freeze
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
# trunk: whether the agent asked about the trunk, which says so while its check is going.
|
|
16
|
+
def self.lines(report, trunk: false)
|
|
15
17
|
needed = Verdict::LEVELS.fetch(report.need)
|
|
16
18
|
[header(report), *report.stages.map { |stage| stage_line(stage, needed) }, *Digest.lines(report.stages, needed),
|
|
17
|
-
*footer(report)]
|
|
19
|
+
*trunk(report, trunk), *footer(report)]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The trunk lines; nothing while the check is going, unless asked about
|
|
23
|
+
# (right after a commit it would say nothing useful).
|
|
24
|
+
def self.trunk(report, asked)
|
|
25
|
+
return [] if report.trunk.nil? || (report.trunk.state == "checking" && !asked)
|
|
26
|
+
|
|
27
|
+
TrunkText.lines(report.trunk, branch: report.branch, next_step: report.deciding.nil?)
|
|
18
28
|
end
|
|
19
29
|
|
|
20
30
|
def self.header(report) = %(fun-ci: #{report.sha[0, 7]} "#{report.subject}" on #{report.branch})
|
|
@@ -27,6 +37,7 @@ module FunCi
|
|
|
27
37
|
|
|
28
38
|
def self.footer(report)
|
|
29
39
|
return ["fun-ci why #{report.sha[0, 7]} #{report.deciding}"] if report.deciding
|
|
40
|
+
return ["fun-ci why #{report.sha[0, 7]} trunk"] if report.trunk&.state == "conflicts"
|
|
30
41
|
return [] unless report.verdict == :superseded && report.superseded_by
|
|
31
42
|
|
|
32
43
|
["Superseded by #{report.superseded_by[0, 7]}."]
|