refactor(tui): resolve the color env overrides at the CLI entry point - #1837
Conversation
|
This pull request is part of a Mergify stack:
|
Merge Protections🟢 All 6 merge protections satisfied — ready to merge. Show 6 satisfied protections🟢 🤖 Continuous Integration
🟢 👀 Review Requirements
🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
🟢 🔎 Reviews
🟢 📕 PR description
🟢 🚦 Auto-queueWhen all merge protections are satisfied, this pull request will be queued automatically. |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved comments require shared-helper reuse, additional color-behavior tests, and documentation clarification.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Moves color-environment resolution to CLI startup so TUI rendering no longer reads the process environment.
Changes:
- Resolves color overrides once and treats empty values as unset.
- Applies the resolved choice to tracing ANSI output.
- Updates related TUI, progress, and project documentation.
File summaries
| File | Summary |
|---|---|
crates/mergify-tui/src/theme.rs |
Uses recorded color choices without environment reads. |
crates/mergify-tui/src/select.rs |
Updates color-policy documentation. |
crates/mergify-tui/src/lib.rs |
Documents environment-independent color handling. |
crates/mergify-stack/src/progress.rs |
Updates progress color-policy documentation. |
crates/mergify-cli/src/main.rs |
Resolves color settings and configures tracing ANSI behavior. |
AGENTS.md |
Documents the color-resolution architecture. |
Review details
Suppressed comments (4)
crates/mergify-cli/src/main.rs:2815
- Please use
mergify_core::env::var_non_emptyfor this flag/env resolution instead of adding another empty-as-unset helper. The shared environment module explicitly requires that helper for everyflag → env → defaultchain (crates/mergify-core/src/env.rs:15-29) and already tests the empty-value behavior; duplicating it here lets the color semantics drift from the workspace funnel.
fn non_empty(name: &str) -> bool {
env::var_os(name).is_some_and(|v| !v.is_empty())
crates/mergify-cli/src/main.rs:729
- This changes user-visible tracing output, but no test covers the new stderr ANSI policy. The existing tests only exercise color-choice folding, so a regression that makes
--color neverorNO_COLORemit escape sequences in-vvlogs would go unnoticed; add focused coverage foransi_on_stderror an actual captured verbose run.
.with_ansi(ansi_on_stderr(color))
crates/mergify-cli/src/main.rs:694
- This changes the documented environment contract:
README.md:216still says thatNO_COLORdisables colored output, but an exported emptyNO_COLORnow intentionally leaves color enabled. Please update the README to document the non-empty requirement so workflow users are not misled.
// Exported-but-empty is not set: a workflow writing
// `NO_COLOR: ${{ inputs.no_color }}` with no input exports the
// empty string, and <https://no-color.org> says that does not
// count. Same rule as every other variable this CLI reads.
crates/mergify-stack/src/progress.rs:114
- This now describes the recorded
--colorchoice as controllingProgress, butProgress::newstill usesTheme::new(false)whenever the output is non-interactive (including--color always) and only callsTheme::detecton the interactive branch. Please document that exception here, or change the implementation if--color alwaysis intended to force color for streamed progress too.
/// read. Color is delegated to [`Theme::detect`] (the recorded
/// `--color` choice, else the TTY; off outside the CLI entry
/// point, so tests are never colored).
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`Theme::detect()` read `NO_COLOR`, `FORCE_COLOR` and `CLICOLOR_FORCE`
on every call, and it is called from production paths in
`mergify-freeze`, `mergify-queue`, `mergify-events` and
`mergify-stack`. So any test in any of those crates read the process
environment, on whatever thread it happened to run on, just by
rendering a themed line. That is the read half of the data race this
stack is about: the tests in `mergify-ci`, `mergify-core`,
`mergify-cli` and `mergify-stack` mutate the process environment
while these read it.
Those three reads could never change the answer inside a test binary
anyway. `resolve_enabled` returns `false` for a `None` choice
whatever they say, and only `mergify-cli`'s entry point ever records
a choice, which the doc comment on `set_color_choice` already said. A
test harness read three environment variables to compute a value it
had already decided.
`mergify-tui` now takes the resolved choice and never reads the
environment. `--color always|never` still wins, `NO_COLOR` still
beats `FORCE_COLOR` / `CLICOLOR_FORCE`, and `auto` with neither still
defers to the TTY. The precedence moves to `resolve_color_choice` in
the binary, next to the clap enum it maps from, which is also where
the workspace's environment funnel lives. `mergify-tui` cannot use
that funnel: it is `mergify-core`, and this crate deliberately stays
dependency-light.
Two behaviour fixes come with it, both in the same three variables:
Exported-but-empty no longer counts as set. A workflow writing
`NO_COLOR: ${{ inputs.no_color }}` with no input exports the empty
string, and every `mergify` in that job lost color with no way back
but `--color always`. <https://no-color.org> says "present and not an
empty string"; so does every other variable this CLI reads.
The log stream obeys the same decision. `init_tracing` was choosing
ANSI from `stderr().is_terminal()` alone, so `NO_COLOR=1` and
`--color never` gave plain stdout and a `-vv` stream still full of
escape sequences, which is the one output people redirect to a file.
The remaining visible difference is *when* the variables are read:
once at startup instead of at every `Theme::detect()`. Nothing
mutates the environment mid-run, so the answer is the same either
way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: Ib099b7044861960ca2c9e85f443072914b333c20
1838eec to
092133b
Compare
Revision history
|
Merge Queue Status
This pull request spent 9 minutes 28 seconds in the queue, including 8 minutes 55 seconds running CI. Required conditions to merge
|
Theme::detect()readNO_COLOR,FORCE_COLORandCLICOLOR_FORCEon every call, and it is called from production paths in
mergify-freeze,mergify-queue,mergify-eventsandmergify-stack. So any test in any of those crates read the processenvironment, on whatever thread it happened to run on, just by
rendering a themed line. That is the read half of the data race this
stack is about: the tests in
mergify-ci,mergify-core,mergify-cliandmergify-stackmutate the process environmentwhile these read it.
Those three reads could never change the answer inside a test binary
anyway.
resolve_enabledreturnsfalsefor aNonechoicewhatever they say, and only
mergify-cli's entry point ever recordsa choice, which the doc comment on
set_color_choicealready said. Atest harness read three environment variables to compute a value it
had already decided.
mergify-tuinow takes the resolved choice and never reads theenvironment.
--color always|neverstill wins,NO_COLORstillbeats
FORCE_COLOR/CLICOLOR_FORCE, andautowith neither stilldefers to the TTY. The precedence moves to
resolve_color_choiceinthe binary, next to the clap enum it maps from, which is also where
the workspace's environment funnel lives.
mergify-tuicannot usethat funnel: it is
mergify-core, and this crate deliberately staysdependency-light.
Two behaviour fixes come with it, both in the same three variables:
Exported-but-empty no longer counts as set. A workflow writing
NO_COLOR: ${{ inputs.no_color }}with no input exports the emptystring, and every
mergifyin that job lost color with no way backbut
--color always. https://no-color.org says "present and not anempty string"; so does every other variable this CLI reads.
The log stream obeys the same decision.
init_tracingwas choosingANSI from
stderr().is_terminal()alone, soNO_COLOR=1and--color nevergave plain stdout and a-vvstream still full ofescape sequences, which is the one output people redirect to a file.
The remaining visible difference is when the variables are read:
once at startup instead of at every
Theme::detect(). Nothingmutates the environment mid-run, so the answer is the same either
way.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com