Skip to main content

juneau-terminal

juneau-terminal is build tooling, not a library dependency. It ships juneau_terminal.py, a stdlib-only Python script that wraps Maven, pytest, Playwright, Gradle, npm and Bazel runs. It parses each tool's output into steps, sub-steps and test counts, and from those it draws a board console view, writes full and board logs, prints terminal-protocol markers for a runner UI, and writes terminal-view events for the Terminal View Region and the Terminal Region. Juneau's own test.py, push.py and release.py run on it.

The module is pom packaging and attaches its files to org.apache.juneau:juneau-terminal:<version>:

TypeClassifierContents
pyjuneau_terminal.py, the engine.
zipbootstrap.py, the juneau_terminal_scripts toolkit for push and release scripts, the pytest plugin and the Playwright reporter.
pypytestThe pytest plugin on its own.

It is not managed by juneau-bom; pin it at the Juneau version your build already uses. mvn install in the module runs its unit tests (python3 -m unittest); -DskipTests skips them. PTY mode is POSIX only.

note

Using the Maven artifact, or a vendored copy, and running mvn install in the module, is the interim way to get juneau-terminal until Juneau 10.0.0 is released. For the 10.0 release the plan is to publish it as a PyPI package, apache-juneau-terminal (installed with uv tool install or pipx), as a runnable .pyz, and as a Homebrew tap (brew install apache/juneau/juneau-terminal, the CLI only). The Playwright reporter is planned for npm as @apache/juneau-terminal-playwright. These are plans, and the details may change.

The module README is the full reference. The wire formats are in terminal-protocol-v1.md and prompt-protocol-v1.md.

Running one command​

python3 juneau_terminal.py [options] <tool> -- <cmd...>

<tool> picks the output parser: maven, pytest, playwright, gradle, npm, bazel or generic. Every option comes before <tool>.

OptionEffect
--console full|board|noneWhere the tool's output goes (see Console view).
--full-log PATH, --board-log PATHThe raw log, and the view's rows as plain text.
--step ID, --title TEXTThe step's id and title.
--pty, --size COLSxROWS, --events PATHRun under a pseudo-terminal (see PTY mode).
--input stdinWith --pty, copy this process's stdin to the tool's terminal.
--prompts stdioWith --pty, relay the job's prompt channel (see Prompts). Not with --input.

From a script, run_tool(cmd, parser, step_id, title, ...) does the same and returns the exit code, the time taken and the test totals. When a script runs a command without naming a parser, it is picked from the command: mvn or mvnw is maven, pytest (direct, python -m pytest or uv run pytest) is pytest, playwright test is playwright, and so on, looking through env, npx, pnpm, yarn and npm exec. Quiet Maven (-q) and anything unrecognized use generic.

Console view​

JUNEAU_TERMINAL_CONSOLE (or --console) chooses the view:

  • full (the default) prints the tool's output as it comes.
  • board draws one row per step, with its time, a progress bar, counts and one line of summary. Each running unit (a Maven module, a pytest or Playwright test file, a check) gets a line under its row. Raw output goes to the full log.
  • none draws nothing. Both of the others still write the logs.

The board view of a Maven run, part-way through and when it has finished:

The board console view of a Maven run, part-way through

The board console view of a finished Maven run

With board or none and no full log named, the full log defaults to ~/.cache/juneau-terminal/<script>-<YYYYMMDD-HHMMSS>.log (the newest 20 are kept), and the header prints the tail -f command for it. JUNEAU_TERMINAL_DETAIL (summary, actionable, modules or all; default actionable) sets how many sub-rows a finished step keeps. Failures, prompts and the final ✅/❌/⚠ line show at every level. Off a terminal, with TERM=dumb or with JUNEAU_TERMINAL_LIVE=0, each row prints once when it ends; NO_COLOR turns off colour. Ctrl-C and SIGTERM close the view and end the run as cancelled (exit 130). A display failure falls back to plainer output and never fails the build.

Progress bars​

A test row's bar has one block per unit:

ParserOne block by defaultbar= choices
mavena module"modules"
pytesta test file"files", "tests"
playwrighta test file"files", "tests"

bar= on run_tool, run.cmd, run.gate, tools.pytest or tools.playwright picks another unit; a value the parser doesn't offer raises ValueError before the command starts. A file block is added when its last test ends: it fails if any test failed, is skipped if all were skipped, and passes otherwise. On a file bar the percent and time estimate follow the tests run, so they move with every test.

Maven rows:

  • One module. A build of a single project, a one-module tree or -pl without --also-make, draws as one module: one in-flight block and 0/1 while it runs, with the percent taken from the tests run against the history's expected count. It counts as 1/1 when it succeeds, so its history is usable for later estimates.
  • No tests. A build that stops before test (only goals up to test-compile, or -DskipTests) still draws its module bar: each module counts when it finishes compiling, and the row shows no test totals.

The pytest and Playwright file blocks need the units' own progress lines:

  • pytest plugin. -p juneau_terminal_pytest is added to every pytest run, with only the plugin's directory put on PYTHONPATH, so the suite under test never sees the engine. JUNEAU_TERMINAL_PYTEST_PLUGIN=0, or -p no:juneau_terminal_pytest on the command, leaves it out.
  • Playwright reporter. juneau_terminal_playwright.cjs is added through PW_TEST_REPORTER, which Playwright appends to the reporter: list in playwright.config, so the config's reporters still run. It draws a line per running test file, counts the bar once per test however often it is retried, and, with markers on and TERMINAL_ARTIFACTS set, writes <step>-playwright.json. Outside a terminal it also sets PLAYWRIGHT_HTML_OPEN=never, so a failing run doesn't serve its html report and wait. It is left out with JUNEAU_TERMINAL_PLAYWRIGHT_REPORTER=0, or when the command names its own --reporter or sets PW_TEST_REPORTER. PW_TEST_REPORTER is undocumented in Playwright: a version that ignores it runs as a plain run, and the step notes "Playwright reporter not loaded".

Without the plugin or the reporter, both bars count tests.

Run history and estimates​

Each run records per-step timings and test counts under ~/.cache/juneau-terminal/history/<repo>-<hash>/, keyed by the repository's git common directory, so branches and linked worktrees share one history. From it a running row shows the expected test total (41,200 / ~83,000 tests), a percent and an estimated total time, and the view keeps an Overall line for the whole run. An estimate changes only when a new one differs by more than 15 seconds or 10%, whichever is larger, and is rounded by size.

VariableEffect
JUNEAU_TERMINAL_HISTORY=0No history: no percent, estimates or Overall line.
JUNEAU_TERMINAL_HISTORY_DIRReplaces ~/.cache/juneau-terminal/history.
JUNEAU_TERMINAL_HISTORY_KEYReplaces the per-repository key, so separate clones of one repository can share a history.

Run-protocol markers​

With TERMINAL_MARKERS=1, stdout carries one ##terminal <json> line per event (run, step, end, tests, report, note, done) for a runner UI, and no view is drawn there; the board log still gets the rows. With it unset the output is what the tool prints. A process started by the one that owns the run (JUNEAU_TERMINAL_ACTIVE=1) adds steps but never its own run or done. When TERMINAL_ARTIFACTS is set, test reports are copied under $TERMINAL_ARTIFACTS/reports/<step>/, so a later clean cannot remove them.

JUNEAU_TERMINAL_MARKERS_FILE sends the same markers to a file while the console view still draws, and JUNEAU_TERMINAL_RESULT names a file that gets the run's outcome as JSON when it ends. A tool that starts a script uses them to follow it; see Driving a Run from Another Tool.

PTY mode and the terminal region​

python3 juneau_terminal.py --pty --full-log LOG --events EVENTS [--size COLSxROWS] <tool> -- <cmd...>

The tool runs under a pseudo-terminal of a fixed size (default 120x40), so tools that colour, redraw or draw progress bars behave as they do in a terminal. LOG gets the tool's bytes unchanged, with a LOG.size sidecar, and EVENTS gets terminal-view events whose rawOffset points into LOG, whether or not markers are on. This is what TerminalProcess runs; the terminal region shows LOG and the terminal view shows EVENTS. The tool's exit code passes through; a tool killed by signal n exits 128+n with a warning note, and one that cannot start exits 127.

Prompts​

juneau_terminal_scripts.prompt asks for input (text, secret, confirm, choice, form and email). When the host started the script with the two prompt descriptors, each prompt is a JSON line to the host and the answer comes back on the other descriptor; otherwise it prompts on the terminal. juneau_terminal.py --pty --prompts stdio relays that channel over its own stdout and stdin: the job's prompt lines go to stdout, and lines on stdin go back to the job. EOF on stdin means the host has gone, and the job is cancelled. A secret answer is never logged, published, checkpointed or put on a command line.

Cancelling​

Every tool runs in a process group of its own. On Ctrl-C or SIGTERM the whole group gets SIGTERM, then SIGKILL for whatever is left after a grace period (10 seconds when a prompt host has gone), so child processes such as forked test JVMs or browsers don't outlive the run. A tool given the real terminal (markers off, no capture) holds the terminal's foreground while it runs. A ^Z stops the run as a whole, and resuming it gives the tool the terminal again.

Writing push and release scripts​

juneau_terminal_scripts, in the zip, holds the helpers a push or release script would otherwise rewrite: git readers and writers, a session with exit codes (0, 1, or 130 for a cancel), tool runners, a step registry with resume, locks, notifications, prompts and a guard that fails a run on stack traces in test output. Policy stays in the script, and the toolkit never runs git add. Vendor the loader once:

unzip -p ~/.m2/repository/org/apache/juneau/juneau-terminal/<v>/juneau-terminal-<v>.zip bootstrap.py > scripts/bootstrap.py
from bootstrap import load
jr, js = load(pom="pom.xml")
from juneau_terminal_scripts import git, run, tools

def main(args):
with run.session("push", bell=True) as s:
s.require(tools.maven(["verify"], title="Build and test"))
s.require(git.commit_index(args.message))
s.require(git.push(set_upstream=True))

load() finds the toolkit through JUNEAU_TERMINAL_SCRIPTS_ZIP and JUNEAU_TERMINAL_PY, then the source tree under the pom's directory, then the local Maven repository, and otherwise copies it there with mvn dependency:copy. With markers off and no terminal the same code prints plain lines, so it works in CI. Each module's docstring is its reference.

Stack-trace guard​

A test run can pass and still print stack traces: a failure that was caught and logged, or a test that provokes one on purpose and never silences it. traces reads what the gates printed and fails the run on any stack trace the repository has not allowed. Take a mark before the gates, then call gate after the last one:

from juneau_terminal_scripts import traces

since = traces.mark() # where this run's output starts in the full log
# ... run the build and test gates ...
code = traces.gate("traces", "Traces", "scripts/trace-allowlist.txt", since=since,
report="target/trace-guard.txt", warn_only=args.warn_traces)
if code:
raise SystemExit(code) # 1: a trace that is not allowed, or an allowlist error

It finds Java traces (a run of two or more frames, or a Caused by: line), JavaScript traces and Python tracebacks, and names each by its header line, the nearest line above the first frame, for example java.lang.IllegalStateException: boom. The allowlist has one entry per line, a regular expression matched against that header and a mandatory reason:

^java\.lang\.IllegalStateException: boom$  # the test asserts this failure path

A line with no reason, an invalid expression or a repeated expression is an error, and an error fails the row even when warn_only is set. After a run that left nothing out, an entry that matched nothing is reported as stale. The report lists every trace found, allowed or not.

The guard reads the run's full log, which the session keeps when the console is board or none. With --console full there is no log, so the row is marked skipped with a warning rather than passing silently.

In the Juneau build​

test.py, push.py and release.py load juneau_terminal.py straight from juneau-terminal/src/main/python/. With --console board (or JUNEAU_TERMINAL_CONSOLE=board), test.py draws the Compile through Traces rows, and push.py runs test.py and adds its own rows around them. A full push.py run shows these rows, in this order:

RowDrawn byWhat it runs
Precheckspush.pyThe committer identity, no unreviewed changes, and a branch that isn't behind its upstream, before any gate.
Trackerpush.pyWith --tracker-audit, the work-tracker audit gate.
Tagspush.pycheck-container-tags.py, the container test tags.
BOMpush.pycheck-bom-completeness.py, that juneau-bom matches the reactor.
Compiletest.pymvn clean install -DskipTests.
Java teststest.pyThe Maven test run, one block per module.
Perftest.pyThe perf guard: the suite's and each module's wall clock against perf-baseline.txt.
Python teststest.pyscripts/tests, the tooling's own pytest suite, with the current test file shown under the row.
Browser teststest.pyThe *_BrowserTest classes under -Pjs-tests. The row always shows, marked skipped with the reason when the tests don't run.
Tracestest.pyThe stack-trace guard: any stack trace the other test.py rows printed that the repository's scripts/trace-allowlist.txt does not allow fails the run. Skipped, with a warning, when the console keeps no full log.
Timingpush.pyThe timing regression report from the run history. Advisory: it never stops the push.
Installpush.pymvn clean package install -DskipTests, marked skipped when test.py has already installed.
Commitpush.pyCommits what is staged in juneau.
Pushpush.pyPushes juneau to its upstream.
Docs buildpush.pyWhen juneau-docs has changes, build-docs.py --skip-maven as a smoke check before its commit.
Docspush.pyCommits what is staged in juneau-docs.
Docs pushpush.pyPushes juneau-docs to its upstream.

--detail on all three scripts sets the detail level.

See also​