Skip to content

feat(bos_build): --show-plan, --skip, --from and modules profiles#1502

Merged
Nikhil (shadowfax92) merged 8 commits into
mainfrom
feat/preset-plan-visibility
Jul 2, 2026
Merged

feat(bos_build): --show-plan, --skip, --from and modules profiles#1502
Nikhil (shadowfax92) merged 8 commits into
mainfrom
feat/preset-plan-visibility

Conversation

@shadowfax92

Copy link
Copy Markdown
Contributor

Summary

  • --show-plan prints the composed step list + required env vars (marked set/missing, never values) for any preset/profile/product/arch — or a --modules/phase-flag pipeline — and exits without needing a chromium checkout. The projection is generated from plan(), so it can never drift.
  • --skip upload,series_patches (CLI) and skip: (profile key) subtract steps after composition — commenting out a step as an operation. CLI and profile skips union; unknown names fail loudly listing valid steps; a valid-but-absent step is a no-op, so saved profiles inherit future preset changes automatically.
  • --from sign_macos resumes the tail of the composed (post-skip) plan — re-run after a failure without recompiling. CLI-only, by design.
  • Profiles may carry modules: [...] as an explicit enumerated-pipeline opt-in ("you own this list now") routed through the DIRECT-mode machinery; planner-owned keys and planner CLI flags are rejected beside it. Shipped profiles stay switch-based (drift-tested).

Design

plan() stays the single source of truth; the file/CLI layer gets subtraction and visibility instead of enumeration. skip joins Switches (validated in resolved(), subtracted as plan()'s last act so composition rules never re-trigger — skipping sign_windows cannot resurrect mini_installer), slice_from is a planner helper the CLI applies, and load_profile returns a Profile carrier so modules-profiles can't masquerade as planner input. --show-plan exits before the banner, before chromium_src resolution, and before env preflight. An adversarial review verified plan() output for skip-free switches is byte-identical to base across a 324-combination switch/arch/platform matrix.

Test plan

  • uv run python -m unittest discover -s bos_build -t . -p "*_test.py" — 333 tests green (46 new: skip/from goldens, Profile parsing + rejection rules, CliRunner projection tests that scrub CHROMIUM_SRC to prove the no-checkout property)
  • uv run ruff check bos_build — clean
  • uv run browseros build --preset release --skip upload --from configure --show-plan with CHROMIUM_SRC unset — prints plan + env, exits 0
  • CI smoke now runs --show-plan on the checkout-less runner

🤖 Generated with Claude Code

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Greptile (@greptileai) review

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Claude (@claude) review

@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown

Claude finished Nikhil (@shadowfax92)'s task in 4m 4s —— View job


Code Review

Tasks

  • Gather context and understand request
  • Read changed files
    • cli/build.py
    • core/planner.py
    • core/resolver.py
    • cli/build_test.py
    • core/planner_test.py
  • Analyze for correctness, edge cases, security, and design
  • Post review findings

Summary

Strong PR. The architecture is clean — plan() stays the single source of truth; the CLI layer only adds subtraction (--skip) and visibility (--show-plan). The 46 new tests are thorough and the CI smoke test neatly proves the no-checkout property. A few minor items below.


Issues

1. plan() calls resolved() on already-resolved switches (double registry lookup)

_resolve_preset explicitly resolves switches at cli/build.py:498, then passes them to plan() which calls resolved() again at core/planner.py:100. Since resolved() calls all_steps() (a registry scan) and validates every skip name, this runs twice per arch in the composed path. It's idempotent so not a bug, but worth noting if this loop grows. One option: accept a pre-resolved sentinel or guard if self.architectures to skip the re-resolution.

2. slice_from() "not in plan" error dumps the full plan inline

core/planner.py:219:

f"Step '{start}' is not in the composed plan "
f"({' → '.join(steps)}); nothing to resume from"

For a full release plan (~15 steps), this is fine. For a larger future plan or a universal plan, this string could become unwieldy in error output. Consider capping or omitting the inline plan dump here, since --show-plan already shows the plan cleanly.

3. test_from_reflected only checks the first step of the slice

cli/build_test.py:91:

self.assertEqual(plan_lines(result.output)[0], "configure")

SliceFromTest.test_slices_composed_plan_from_step in planner_test.py fully verifies the slice logic, so this isn't hiding a bug. But the CLI-level test only proves the slice started at configure — an accidental off-by-one (e.g., starting at the step after configure) would pass. Suggest:

self.assertEqual(plan_lines(result.output),
    plan(Switches(preset="release"), "x64", get_platform())[plan(...).index("configure"):])

Fix this →

4. No test for bare profile name resolution (--profile nightly-ci)

_resolve_profile_path at cli/build.py:677-686 has a fallback: if the path doesn't exist, try {package_root}/bos_build/profiles/{name}.yaml. All tests in ModulesProfileCliTest pass absolute temp-file paths, so the bare-name lookup path has no CLI-level coverage. The test_shipped_nightly_ci_profile_matches_ci_switches test in planner_test.py uses the full path directly. Adding one test that passes "nightly-ci" as a bare name via CliRunner would close this gap.

5. Deferred EnvConfig imports inside function bodies

cli/build.py:284, cli/build.py:606, and _resolve_chromium_src all do from ..core.env import EnvConfig inside function bodies. If this is intentional (lazy import to keep --show-plan fast, or to avoid a circular import), a comment explaining why would help. Otherwise, moving them to the module top-level is cleaner.

6. Multi-arch --from with a step absent in one arch has no test

If --arch arm64,x64 is used with --from series_patches, and one arch's plan doesn't contain series_patches, slice_from raises correctly. But this is only covered by unit-level SliceFromTest — there's no CLI-level multi-arch --from test at all. Low-priority given the unit coverage, but worth noting.


Minor Observations (non-blocking)

  • dict.fromkeys for skip union (cli/build.py:496): correct; preserves insertion order and deduplicates. Profile skip leads, CLI skip appends.
  • isinstance(name, str) guard in resolved() (core/planner.py:70): good defensive coding against skip: [upload: true] YAML surprises.
  • yaml.safe_load used correctly throughout — no yaml.load unsafe patterns.
  • _display_only_runs sentinel (cli/build.py:409): clean fail-fast guard for programming errors.
  • Empty plan allowed in --show-plan, rejected in build_runs(): intentional and tested. The asymmetry is clearly documented in EmptyPlanTest.
  • build_runs() captures arch_plans by reference: safe since the list is never modified after the closure is defined.
  • quiet parameter in resolve_pipeline: minimal, correct change.

Verdict

The design decisions are sound and well-justified. Items 1–2 are clean-up candidates; items 3–5 are low-risk test coverage gaps. Nothing blocks merging.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds --show-plan, --skip, and --from CLI flags to the bos_build tool, along with support for modules: lists inside profile YAML files. All three features are composed on top of the existing plan() function so the displayed plan can never drift from what actually runs.

  • --show-plan exits before resolving CHROMIUM_SRC, printing the composed step list and env-var markers (set/missing, never values); the CI smoke test on a checkout-less runner validates this guarantee.
  • --skip (and the profile skip: key) subtracts steps after composition using a validated deduplicating union, so removing a step never re-triggers composition rules; --from slices the post-skip plan for resuming after failures.
  • modules: profiles route to direct-mode machinery, reject all planner-owned keys, and are restricted to single-arch to avoid ambiguity; shipped profiles are explicitly tested to remain switch-based.

Confidence Score: 4/5

Safe to merge. The new flags are additive and the refactoring of _resolve_preset_runs into the lazy _PlanProjection pattern correctly defers checkout resolution. All error paths reach typer.Exit(1) through existing or new try/except blocks.

The implementation is well-structured: plan() remains the single source of truth, skip validation fires in resolved() before any subtraction, slice_from is applied after skip, and --show-plan exits before touching the checkout. The 46 new tests cover skip/from goldens, Profile parsing rejection, and CLI projection tests that scrub CHROMIUM_SRC. The three findings are minor style and UX issues: the slice_from absent-step error message joins all plan steps inline (potentially 20+ steps), one golden test hardcodes a plan tail that will break silently if steps are added, and _PlanProjection declares frozen=True while holding mutable List fields.

No files require special attention. The largest change is cli/build.py where _resolve_preset was rearchitected, but the refactoring is consistent and well-tested.

Important Files Changed

Filename Overview
packages/browseros/bos_build/core/planner.py Adds skip field to Switches, post-composition subtraction in plan(), new slice_from(), Profile dataclass, updated load_profile() returning Profile, and _as_tuple helper. Logic is sound; slice_from error message verbosity is a minor UX concern.
packages/browseros/bos_build/cli/build.py Adds --skip, --from, --show-plan CLI flags; refactors _resolve_preset_runs into _resolve_preset returning a lazy _PlanProjection; adds _resolve_modules_profile and _print_plan. frozen=True with List fields is a minor code smell.
packages/browseros/bos_build/core/planner_test.py Adds 80+ lines of new tests covering skip subtraction, slice_from, profile parsing (including modules profiles), and Profile rejection rules. One test hardcodes a plan tail slice, making it fragile to plan changes.
packages/browseros/bos_build/cli/build_test.py New 276-line CLI test file covering show-plan, skip, from, modules-profile, and mode-guard scenarios; all tests scrub CHROMIUM_SRC/ARCH to prove no-checkout property. Coverage is thorough.
packages/browseros/bos_build/core/resolver.py Adds quiet parameter to resolve_pipeline to suppress logging during --show-plan projections. Minimal, correct change.
.github/workflows/bos-build-tests.yml Adds --show-plan smoke test to CI, running on the checkout-less runner to verify the no-checkout guarantee in CI. Clean addition.
packages/browseros/bos_build/README.md Documents the new --show-plan, --skip, --from, and modules-profile features with examples and design rationale. Accurate and well-written.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[CLI invocation] --> B{has_preset?}
    B -- No --> C[resolve_pipeline\nquiet=show_plan]
    C --> D{show_plan?}
    D -- Yes --> E[validate_pipeline\n_print_plan\nexit 0]
    D -- No --> F[resolve_config\nbuild runs]

    B -- Yes --> G[_resolve_preset]
    G --> H{profile has\nmodules:?}
    H -- Yes --> I[_resolve_modules_profile\nreject planner flags\nvalidate_pipeline]
    H -- No --> J[merge overrides\nunion skip\nswitches.resolved]
    J --> K[plan per arch\nslice_from if --from]
    I --> L[_PlanProjection\nbuild_runs closure]
    K --> L

    L --> M{show_plan?}
    M -- Yes --> N[_print_plan\nenv markers\nexit 0\nno checkout needed]
    M -- No --> O[projection.build_runs\n_resolve_chromium_src\nContext construction\nexecute pipeline]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[CLI invocation] --> B{has_preset?}
    B -- No --> C[resolve_pipeline\nquiet=show_plan]
    C --> D{show_plan?}
    D -- Yes --> E[validate_pipeline\n_print_plan\nexit 0]
    D -- No --> F[resolve_config\nbuild runs]

    B -- Yes --> G[_resolve_preset]
    G --> H{profile has\nmodules:?}
    H -- Yes --> I[_resolve_modules_profile\nreject planner flags\nvalidate_pipeline]
    H -- No --> J[merge overrides\nunion skip\nswitches.resolved]
    J --> K[plan per arch\nslice_from if --from]
    I --> L[_PlanProjection\nbuild_runs closure]
    K --> L

    L --> M{show_plan?}
    M -- Yes --> N[_print_plan\nenv markers\nexit 0\nno checkout needed]
    M -- No --> O[projection.build_runs\n_resolve_chromium_src\nContext construction\nexecute pipeline]
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/browseros/bos_build/core/planner.py:874-879
**Verbose error message for absent `--from` step**

The error message inline-joins all plan steps with ``, which for a full release plan can produce a 20+ step single-line string that is hard to read in a terminal. If the plan grows, this becomes nearly unreadable. Consider either truncating to the first/last few steps or directing the user to `--show-plan` instead of printing the full plan inline.

### Issue 2 of 3
packages/browseros/bos_build/core/planner_test.py:1117-1121
**Hardcoded plan tail makes the test fragile to plan changes**

`test_slices_composed_plan_from_step` asserts an exact slice `["sign_macos", "package_macos", "upload"]`. If new steps are inserted into the release arm64/macos plan between `sign_macos` and `upload`, this test breaks with a confusing diff rather than a signal about `slice_from` itself. Consider deriving the expected value from `plan()` directly: `full = plan(RELEASE, "arm64", "macos"); expected = full[full.index("sign_macos"):]`.

### Issue 3 of 3
packages/browseros/bos_build/cli/build.py:253-263
**`frozen=True` with mutable `List` fields gives a misleading immutability signal**

`_PlanProjection` is `frozen=True`, which prevents attribute rebinding but does not protect the contents of `header: List[str]` and `arch_plans: List[Tuple[str, List[str]]]`. A reader seeing `frozen=True` may assume the object is deeply immutable. Since the class is private and single-use, changing the field types to `Tuple` (e.g., `Tuple[str, ...]` for `header`) would make the invariant accurate, or alternatively dropping `frozen=True` would remove the false signal.

Reviews (1): Last reviewed commit: "fix(bos_build): tighten plan projection ..." | Re-trigger Greptile

Re-applies skip/slice/Profile on top of the plan_runs universal refactor
(#1501): skip subtraction now covers the universal runs, --from resumes
the sequential run timeline (slice_runs_from) so a failed universal merge
restarts at merge_universal without recompiling, and the README documents
the timeline semantics.
@shadowfax92 Nikhil (shadowfax92) merged commit f54368c into main Jul 2, 2026
4 checks passed
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces --show-plan, --skip, and --from flags to the build CLI, plus a modules: opt-in for profiles — all built around the principle that plan() remains the single source of truth and the file/CLI layer only subtracts from or displays what it composes.

  • --show-plan projects the full composed step list and required env-var markers without touching the chromium checkout; it exits before the banner, verified by the checkout-less CI smoke run.
  • --skip (and profile skip:) subtracts steps after composition so skipping one step can never re-trigger planner rules; CLI and profile skips are unioned, unknown names fail loudly, absent-but-valid names are no-ops.
  • --from slices the post-skip plan from a given step to support resuming after a failure; CLI-only by design, applied through slice_from() which validates registry membership before checking plan membership.
  • Modules profiles route through DIRECT-mode machinery, reject all planner-owned keys, and are enforced to be single-arch and non-empty at parse time; shipped profiles are drift-tested to never carry modules:.

Confidence Score: 4/5

The change is safe to merge; all new flags are additive and normal builds follow the identical path as before — plan() output for skip-free switch combinations is byte-identical to the base, verified across a 324-combination matrix.

The redesign is well-structured and heavily tested (333 tests, 46 new). The main risk surface is the deferred build_runs closure inside _resolve_preset, which captures pre-computed arch plans across the show-plan boundary — but the logic is correct and the closure's empty-plan guard fires before the checkout is touched. The three findings are all style/quality items that do not affect correctness.

The restructured build.py — specifically _resolve_preset, _resolve_modules_profile, and the _PlanProjection closure pattern — benefits from a close read to confirm the deferred execution contract holds as the file evolves.

Important Files Changed

Filename Overview
packages/browseros/bos_build/cli/build.py Core CLI change: adds --show-plan, --skip, --from flags and restructures _resolve_preset_runs into a lazy _PlanProjection with deferred build_runs closure so plan output never requires a chromium checkout. Logic is correct; minor style notes on lazy EnvConfig imports and sentinel default.
packages/browseros/bos_build/core/planner.py Adds skip field to Switches (validated in resolved()), skip subtraction as plan()'s final act, slice_from helper, Profile carrier dataclass, and load_profile extension for modules profiles with banned planner-key enforcement. Well-structured with clear invariants.
packages/browseros/bos_build/cli/build_test.py New test file with 46 tests covering show-plan, skip, from, and modules-profile CLI paths; scrubs CHROMIUM_SRC/ARCH to prove checkout-free guarantee. The combined() helper catches ValueError alongside AttributeError which is slightly broader than necessary.
packages/browseros/bos_build/core/planner_test.py Adds SkipTest, SliceFromTest, and expanded ProfileTest cases including golden tests, edge-case handling for non-string skip entries, and a drift test asserting shipped profiles never carry modules:.
packages/browseros/bos_build/core/resolver.py Minimal change: adds quiet parameter to resolve_pipeline to suppress DIRECT MODE log lines during --show-plan projections. Correct and non-breaking.
.github/workflows/bos-build-tests.yml Adds --show-plan smoke test on the checkout-less CI runner; intentionally exercises the no-checkout path.
packages/browseros/bos_build/README.md Documentation update describing --show-plan, --skip, --from, and the modules-profile opt-in pattern; accurate and matches the implementation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[browseros build CLI] --> B{has_preset?}
    B -->|yes| C[_resolve_preset]
    B -->|no| D[resolve_pipeline]

    C --> E{profile has modules:?}
    E -->|yes| F[_resolve_modules_profile\nvalidate_pipeline\neff_arch/build_type]
    E -->|no| G[load switch-based profile\nmerge CLI overrides\nunion skip sets\nswitch.resolved]

    G --> H[plan per arch\nslice_from if --from]
    F --> I[_PlanProjection\nheader + arch_plans + build_runs closure]
    H --> I

    I --> J{show_plan?}
    J -->|yes| K[_print_plan: steps + env markers\nexit 0 no checkout needed]
    J -->|no| L[projection.build_runs\nresolve chromium_src\nbuild Context per arch]

    D --> M{show_plan?}
    M -->|yes| N[validate_pipeline\nprint direct plan\nexit 0]
    M -->|no| O[resolve_config\nContext per arch]

    L --> P[validate_pipeline\npreflight\nrun_pipeline]
    O --> P
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[browseros build CLI] --> B{has_preset?}
    B -->|yes| C[_resolve_preset]
    B -->|no| D[resolve_pipeline]

    C --> E{profile has modules:?}
    E -->|yes| F[_resolve_modules_profile\nvalidate_pipeline\neff_arch/build_type]
    E -->|no| G[load switch-based profile\nmerge CLI overrides\nunion skip sets\nswitch.resolved]

    G --> H[plan per arch\nslice_from if --from]
    F --> I[_PlanProjection\nheader + arch_plans + build_runs closure]
    H --> I

    I --> J{show_plan?}
    J -->|yes| K[_print_plan: steps + env markers\nexit 0 no checkout needed]
    J -->|no| L[projection.build_runs\nresolve chromium_src\nbuild Context per arch]

    D --> M{show_plan?}
    M -->|yes| N[validate_pipeline\nprint direct plan\nexit 0]
    M -->|no| O[resolve_config\nContext per arch]

    L --> P[validate_pipeline\npreflight\nrun_pipeline]
    O --> P
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/browseros/bos_build/cli/build_test.py:30-33
The `except (ValueError, AttributeError)` in `combined()` catches `ValueError` alongside `AttributeError`. `AttributeError` is the right guard for older Click versions where `.stderr` may not exist, but `ValueError` is unrelated to attribute access and could silently swallow a genuine infrastructure error. Narrowing to `AttributeError` only keeps the intent precise.

```suggestion
    try:
        out += result.stderr
    except AttributeError:
        pass
```

### Issue 2 of 3
packages/browseros/bos_build/cli/build.py:25
`EnvConfig` is imported lazily inside both the `--show-plan` direct-mode branch and `_resolve_modules_profile`. Since `build.py` already has a rich set of top-level imports and `EnvConfig` is not heavyweight, a top-level import is cleaner and makes the dependency visible at a glance.

```suggestion
from ..core.env import EnvConfig
from ..core.resolver import resolve_config, resolve_pipeline
```

### Issue 3 of 3
packages/browseros/bos_build/cli/build.py:249-263
**`_display_only_runs` sentinel vs. `Optional` default**

`_display_only_runs` raises `RuntimeError` as a guard against callers that create a `_PlanProjection` without a real `build_runs`. A caller who accidentally omits the argument in a future code path gets a `RuntimeError` with a message pointing to display semantics rather than the actual mistake. An `Optional[Callable]` field defaulting to `None` with an explicit `None`-check at the call site would produce a more actionable error and make the intent clear in the type signature.

Reviews (2): Last reviewed commit: "chore: merge main into feat/preset-plan-..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant