fun_ci 2.0.1-aarch64-linux → 2.1.0-aarch64-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 +101 -0
- data/README.md +33 -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/gradle_linter_detector.rb +23 -0
- data/lib/fun_ci/setup/installer.rb +10 -10
- data/lib/fun_ci/setup/lint_detection.rb +32 -0
- data/lib/fun_ci/setup/maven_linter_detector.rb +4 -2
- 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/stage_templates/managed.rb +12 -7
- data/lib/fun_ci/setup/stage_templates/native.rb +1 -1
- data/lib/fun_ci/setup/stage_templates/shell.rb +12 -3
- data/lib/fun_ci/setup/stage_templates.rb +1 -1
- 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
- data/libexec/fun-ci-renderer +0 -0
- metadata +32 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bb4d24e9a9219546727c463c5df0f702cdf7a08d5be7da0c73983764e1e35c4
|
|
4
|
+
data.tar.gz: 8a1ce072e13d6f147b78698cfbb6f0ad27a1a876f9de9c40ca7756cfee8a0239
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8363c78eb29639cca19887eea9cf0001405e8ced3e7ed4f39ac62a72fe72f72bd7bd4cae119cfc2db11030954008fdcf58017ab6950d0999a2d11d0ecbda388c
|
|
7
|
+
data.tar.gz: 03673b7c256a5cac45ded403f299c231db52853634086672e5aad6e4610f87b7ee04e440bba15e158772b1ee045d9ec32c3b03a70c51dc1cdcec5b6dee8422db
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,107 @@ 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
|
+
|
|
79
|
+
## [2.0.2] - 2026-09-29
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
- In a Gradle project set up by `fun-ci init`, the fast suite could fail
|
|
83
|
+
before running a test, with Gradle unable to hash a file in its own build
|
|
84
|
+
directory (`build/kotlin/compileTestKotlin/...`). The fast and slow suites
|
|
85
|
+
run side by side in one worktree, and both compiled the test code into the
|
|
86
|
+
same place, each deleting what the other was reading. The build script now
|
|
87
|
+
compiles the test code (`./gradlew assemble testClasses`), so neither suite
|
|
88
|
+
has to. Maven, .NET, Elixir, Dart and Swift projects could clash the same
|
|
89
|
+
way, and their scripts change too: Maven builds with `mvn test-compile` and
|
|
90
|
+
runs `mvn surefire:test` and `mvn failsafe:integration-test
|
|
91
|
+
failsafe:verify`, so its slow suite now runs only the integration tests
|
|
92
|
+
failsafe finds (the classes named `*IT`) rather than every test again;
|
|
93
|
+
Elixir compiles its test environment in the build; Dart precompiles its test
|
|
94
|
+
runner and runs the slow suite from source; the .NET and Swift suites run
|
|
95
|
+
with `--no-build` and `--skip-build`.
|
|
96
|
+
- Lint runs side by side with the build, and the lint `fun-ci init` wrote for
|
|
97
|
+
Maven (`mvn verify -DskipTests`) and .NET (`dotnet format`) compiled into
|
|
98
|
+
the same directories as the build. Lint now works on the source code alone.
|
|
99
|
+
Maven's runs the linter the POM configures (detekt, ktlint, checkstyle or
|
|
100
|
+
PMD) or else `mvn validate`, and no longer picks spotbugs, which reads
|
|
101
|
+
compiled classes. .NET's checks whitespace with `dotnet format whitespace
|
|
102
|
+
--folder`. Gradle's runs ktlint, spotless or detekt when the build applies
|
|
103
|
+
one, and otherwise says there is none, where it ran `check`, which compiles.
|
|
104
|
+
A linter that needs compiled code goes in `build.sh` or `fast.sh`.
|
|
105
|
+
- Which stages run side by side, and so what each may read and write, is now
|
|
106
|
+
spelled out: in a new section of `docs/design.md`, in the README, in what
|
|
107
|
+
`fun-ci init` prints, and in a comment at the top of each script it writes.
|
|
108
|
+
Scripts already in `.fun-ci/` are left as they are, so an existing project
|
|
109
|
+
picks up these fixes by editing them to match `docs/stacks.md`.
|
|
110
|
+
|
|
10
111
|
## [2.0.1] - 2026-09-29
|
|
11
112
|
|
|
12
113
|
### Fixed
|
data/README.md
CHANGED
|
@@ -43,6 +43,8 @@ Upgrading from 1.x? Read [Upgrading to 2.0](#upgrading-to-20) first.
|
|
|
43
43
|
| Fast | 10 s | after lint and build pass | the push |
|
|
44
44
|
| Slow | 5 min | in the background, beside the fast suite | nothing |
|
|
45
45
|
|
|
46
|
+
Lint and build run side by side on the commit's checkout, and then the fast and slow suites run side by side on what the build left, all in one worktree. Two stages running at once must not write the same files, so `lint.sh` works on the source code only and `build.sh` compiles everything the suites need, test code included. A linter that needs compiled code belongs in `build.sh` or `fast.sh`. The scripts `fun-ci init` writes keep to this; [Stages side by side](docs/design.md#stages-side-by-side) has the details.
|
|
47
|
+
|
|
46
48
|
A stage that overruns its budget is killed and reported as over budget, in yellow rather than red: a fast suite that takes 12 seconds has grown too heavy, which is a different problem from a failing test.
|
|
47
49
|
|
|
48
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.
|
|
@@ -57,7 +59,9 @@ Results are kept in SQLite under `$XDG_STATE_HOME/fun-ci/` (`~/.local/state/fun-
|
|
|
57
59
|
fun-ci console
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
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
|
+

|
|
61
65
|
|
|
62
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.
|
|
63
67
|
|
|
@@ -84,10 +88,12 @@ Five minutes after the latest run finished, the header goes quiet: a starry nigh
|
|
|
84
88
|
|---|---|
|
|
85
89
|
|  |  |
|
|
86
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
|
+
|
|
87
93
|
Keys:
|
|
88
94
|
|
|
89
95
|
- `j` and `k`, or the arrow keys, move the cursor down and up
|
|
90
|
-
- `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
|
|
91
97
|
- `q` quits
|
|
92
98
|
|
|
93
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.
|
|
@@ -114,6 +120,7 @@ The section `fun-ci init` writes into `AGENTS.md` tells an agent to run that com
|
|
|
114
120
|
| 3 | Still undecided (`wait --within 30s` gives up at your deadline) |
|
|
115
121
|
| 4 | Superseded by a newer commit on the branch (`wait --follow-branch` moves on to it) |
|
|
116
122
|
| 5 | No run for the commit |
|
|
123
|
+
| 6 | Passed, but conflicts with the trunk (only with `--trunk`) |
|
|
117
124
|
| 64 | Usage error |
|
|
118
125
|
|
|
119
126
|
A failed stage comes with its evidence. `status` and `wait` end with a digest and the command that shows the rest:
|
|
@@ -159,6 +166,27 @@ The whole output: fun-ci why 2330979 fast --raw
|
|
|
159
166
|
|
|
160
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.
|
|
161
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
|
+
|
|
162
190
|
### What fun-ci keeps when a stage fails
|
|
163
191
|
|
|
164
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.
|
|
@@ -195,9 +223,9 @@ fun-ci install-hooks Install post-commit and pre-push
|
|
|
195
223
|
fun-ci install-hooks post-commit Install a single hook type
|
|
196
224
|
fun-ci check Verify .fun-ci/ setup
|
|
197
225
|
fun-ci console Watch the runs
|
|
198
|
-
fun-ci status [commit]
|
|
226
|
+
fun-ci status [commit] [--trunk] Where a commit's run stands; the verdict is the exit code
|
|
199
227
|
fun-ci wait [commit] Wait until that verdict is decided, then exit with it
|
|
200
|
-
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
|
|
201
229
|
fun-ci runs This project's recent runs, newest first
|
|
202
230
|
fun-ci events [--follow] The runs' events as JSON lines
|
|
203
231
|
fun-ci extract <stage> --output <file> Try a stage's extractors on a saved output
|
|
@@ -213,7 +241,7 @@ fun-ci prune Remove fun-ci's worktrees when n
|
|
|
213
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.
|
|
214
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.
|
|
215
243
|
- The database moved from `$TMPDIR` to `$XDG_STATE_HOME/fun-ci/`. Runs recorded by 1.x are not carried over.
|
|
216
|
-
- `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`.
|
|
217
245
|
- The console needs `fun-ci-renderer`, which the platform gems include (see [Watching: the console](#watching-the-console)).
|
|
218
246
|
|
|
219
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" }
|