Skip to content

feat(registry): auto-inject Breaks: on Homarr stack for routed visible apps#203

Merged
mairas merged 4 commits into
mainfrom
feat/inject-homarr-stack-breaks
May 13, 2026
Merged

feat(registry): auto-inject Breaks: on Homarr stack for routed visible apps#203
mairas merged 4 commits into
mainfrom
feat/inject-homarr-stack-breaks

Conversation

@mairas
Copy link
Copy Markdown
Contributor

@mairas mairas commented May 13, 2026

Implements docs/plans/2026-05-13-001-feat-container-app-breaks-homarr-stack-plan.md. Closes the silent-missing-card failure mode after the path-only TOML migration.

Why

CPT v0.5.8 (#202) made routed apps emit path-only Homarr card URLs (url = "/{app_id}/"). The path-only TOML requires:

  • homarr-container-adapter (>= 0.4.6)validate_app_url accepts path-only.
  • halos-core-containers (>= 0.3.2) — bundled Homarr fork image accepts path-only appHrefSchema.

Without protection, a user who installs a routed app individually (Cockpit App Store, raw apt install, any partial upgrade) onto an older system gets the new path-only TOML on a peer that can't process it. The card silently does not appear. No error, no upgrade prompt.

Workspace policy 2026-04-30-skip-apt-depends-pins-sibling-halos-packages.md explicitly names "manual partial upgrades are an expected operational pattern" as a Keep-the-pin condition.

What

Breaks: (not Depends:) is the right primitive: conditional on the peer being installed. A HaLOS device running Signal K without Homarr is unaffected; a device with too-old Homarr gets a clean apt error or auto-upgrade resolution.

Generator-injected (not per-app metadata) because the trigger condition is identical to the trigger for path-only TOML emission (routing is not None and web_ui.enabled and web_ui.visible). Single source of truth; future minimum-version bumps are one diff in template_context.py.

Changes

File Purpose
src/schemas/metadata.py New optional breaks: list[str] | None field on AppMetadata.
src/generate_container_packages/template_context.py Two named constants (HOMARR_ADAPTER_MIN_VERSION = "0.4.6", HALOS_CORE_CONTAINERS_MIN_VERSION = "0.3.2") + _compute_homarr_stack_breaks helper + injection in _build_package_context. App-declared breaks compose with (don't replace) auto-injected entries.
src/generate_container_packages/templates/debian/control.j2 New conditional Breaks: block, mirroring the existing Conflicts: shape.
AGENTS.md Future-contributor note: trigger condition, where the constants live, when to bump.
tests/test_models.py 3 schema acceptance tests.
tests/test_template_context.py 10 injection-trigger-matrix tests covering routing/web_ui/visible combinations + app-declared compose.
tests/test_renderer.py 1 new (Breaks line in routed-visible control) + 1 strengthened (no Breaks line in non-routed control).
docs/plans/2026-05-13-001-... Full design document, including the Breaks-vs-Depends rationale and the cohort-vs-partial-upgrade distinction.

Tests

  • 577 unit + 27 integration pass (up from 562 + 27 in v0.5.8). 1 skip is the pre-existing dpkg-buildpackage not available on macOS dev machines.
  • 15 new tests cover: schema accepts breaks: list[str] and rejects scalars; injection fires on routed+enabled+visible and does not fire when any of the three is missing; app-declared breaks compose after auto-injected entries; explicit None doesn't error; rendered control file contains the expected Breaks: line for routed-visible apps and contains no Breaks: line otherwise.
  • Lint, format-check, and typecheck pass. (Typecheck shows the same 2 pre-existing diagnostics in converters/casaos/transformer.py and routing.py that exist on main — not introduced by this PR.)

Why minor (0.6.0), not patch (0.5.9)

New user-facing schema field (breaks) + new observable behavior (auto-injected Breaks lines in every routed visible app's .deb). Backward-compatible feature, which is the textbook minor bump.

After merge

  1. Publish v0.6.0+1 draft as Latest → apt.halos.fi pipeline.
  2. Supersede halos-org/halos-marine-containers#171 — that PR only bumped revisions for path-only TOMLs; with this generator change, the same rebuild also injects the Breaks lines. One marine rebuild covers both.
  3. Update the workspace policy doc with the worked example ("When CPT auto-injects Breaks: for routed visible apps"). Separate PR in halos-org/halos.
  4. Bump halos-pi-gen for the next image build.

mairas added 3 commits May 13, 2026 16:57
…e apps

CPT v0.5.8 made routed apps emit path-only Homarr card URLs
(`url = "/{app_id}/"`). The path-only TOML form requires
`homarr-container-adapter >= 0.4.6` (to accept path-only URLs in
`validate_app_url`) and `halos-core-containers >= 0.3.2` (whose
bundled Homarr fork image accepts path-only `appHrefSchema`).

Without protection, a user who installs a routed container app onto
an older system (Cockpit App Store install, raw `apt install`, or
any partial upgrade) gets the new path-only TOML on a system that
can't process it. The card silently does not appear.

Auto-inject Debian `Breaks: foo (<< X)` clauses for routed visible
apps. `Breaks` is conditional — only enforced when the named peer
is installed. apt either auto-upgrades the peer to satisfy or
refuses the install with a clear error. Does not force the peer to
be installed on systems that don't run Homarr.

Trigger condition mirrors `registry.generate_registry_toml`'s
routed-branch precondition (`routing is not None and
web_ui.enabled and web_ui.visible`) so producer and consumer stay
in lock-step.

App authors may declare additional `breaks:` entries in
`metadata.yaml`; they compose with (do not replace) the
auto-injected entries.

See docs/plans/2026-05-13-001-feat-container-app-breaks-homarr-stack-plan.md
for the design rationale, including why `Breaks` over `Depends`,
why generator-injected over per-app metadata, and how this case
flipped the workspace policy from drop-the-pin to keep-the-pin.

Tests: 15 new (model acceptance + injection trigger matrix + template
rendering). Full suite passes (577 unit + 27 integration).
Future contributors maintaining the generator should know:
- which condition triggers auto-injection,
- where the version constants live, and
- when to bump them.
@mairas
Copy link
Copy Markdown
Contributor Author

mairas commented May 13, 2026

CE Review — Code-review pass

8 reviewer personas spawned in parallel: correctness, testing, maintainability, project-standards, agent-native, learnings-researcher, api-contract, kieran-python.

Verdict: Ready with one substantive change + a small handful of polish items.

One issue surfaced by multiple reviewers independently (correctness, maintainability, kieran-python, agent-native) deserves a fix before merge. Everything else is optional polish or follow-up.


P1 — Predicate drift between _compute_homarr_stack_breaks and registry.generate_registry_toml

Convergent finding (4 reviewers, confidence 0.65–0.85).

The trigger I implemented requires routing is not None AND web_ui.enabled AND web_ui.visible. The path-only-URL emission in registry.generate_registry_toml only requires routing is not None AND web_ui.enabledvisible is written into the TOML at the visibility line, it does not gate emission.

Concrete consequence: a routed, enabled, non-visible app (a legitimate pattern for backend services that need Traefik routing for OIDC callbacks or cross-app calls but don't appear as Homarr cards) gets:

  1. A path-only TOML on disk (per registry.py).
  2. No Breaks: line in its .deb (per _compute_homarr_stack_breaks).
  3. On an old homarr-container-adapter (< 0.4.6): validate_app_url rejects the path-only URL, the adapter logs a tracing::warn, and the TOML entry is silently skipped — breaking that app's adapter-driven health-check ping with no apt-level signal.

The user-visible "No app card" failure mode is the headline case and is covered correctly, but the broader contract (any path-only TOML must be loadable by the adapter) is not. The plan's own goal — "the trigger condition mirrors registry.generate_registry_toml's routed-branch precondition so the two stay in lock-step" — is violated as written.

Recommended fix (combines correctness + maintainability findings): extract a single shared predicate, e.g. emits_path_only_url(metadata) returning routing is not None and (web_ui or {}).get("enabled"), exported from registry.py or a new small helper module, consumed by both registry.generate_registry_toml and _compute_homarr_stack_breaks. That:

  • Closes the silent-failure gap for non-visible routed apps.
  • Eliminates the duplicated condition (and the maintainer-discipline-only "stay in lock-step" comment).
  • Makes future contract changes (e.g., a new gate on web_ui.path) automatically propagate to both Breaks injection and TOML emission.

A drop-in visible removal in _compute_homarr_stack_breaks is a narrower fix that closes the correctness gap without addressing the duplication. Either works; the shared-predicate version is sturdier.

If we keep the current behaviour intentionally, the AGENTS.md note overstates predicate alignment ("whenever the app's TOML will be written with a path-only url") and should be reworded.


P2 — Plan doc references a file that isn't in the workspace

learnings-researcher caught this. The plan's Sources & References section links:

docs/solutions/best-practices/2026-05-04-ship-cross-format-identity-helper-before-url-migration.md

…and labels it a workspace doc. The actual file is in homarr-container-adapter/docs/solutions/best-practices/, not the halos-org/halos workspace. The workspace's solutions dir holds only:

  • 2026-04-30-skip-apt-depends-pins-sibling-halos-packages.md (correctly cited)
  • 2026-05-04-image-swap-smoke-test-without-deb-rebuild.md
  • 2026-05-04-verify-plan-against-repo-state-before-execution.md

Fix: either point the cited URL at the adapter repo's docs/, or drop the reference and replace it with the 2026-04-30 skip-apt-depends doc's Keep-the-pin branch (which the learnings reviewer correctly noted is the precedent this PR rests on — the existing doc names "manual partial upgrades are an expected operational pattern" as a Keep condition, and PR #203 is the concrete realisation of that case).


P2 — Untested trigger-matrix edge: routing: {} (empty dict)

testing reviewer caught this. _compute_homarr_stack_breaks uses is None, so a present-but-empty routing: {} short-circuits past the guard and the function proceeds. None of the 10 tests in TestHomarrStackBreaksInjection exercise this — they either delete routing entirely or leave the populated {"auth": {"mode": "none"}} value. If a future schema change makes routing default to {} for some path, the trigger flips silently.

Easy add: one test asserting routing: {} still injects, mirroring the production behaviour. If we end up extracting the shared predicate above, this case naturally falls out of the predicate's test fixture.


P3 — Optional polish

These are all "fix if straightforward, otherwise drop":

  • Constant naming asymmetry (maintainability, kieran-python, weak signal): HOMARR_ADAPTER_MIN_VERSION abbreviates homarr-container-adapter while HALOS_CORE_CONTAINERS_MIN_VERSION mirrors its package name verbatim. Renaming to HOMARR_CONTAINER_ADAPTER_MIN_VERSION makes the constant trivially greppable from the rendered Breaks: line and the package name in CI logs. The injected relationship string and AGENTS.md text both already use the full name.
  • Pydantic breaks field example may drift (agent-native): the field description embeds 'homarr-container-adapter (<< 0.4.6)', the same literal as the auto-injected constant. If the constant is bumped per AGENTS.md, the schema example silently falls out of sync. Replace with a non-Homarr example (e.g., 'some-peer-package (<< 1.0)') so the schema doesn't pretend to be authoritative for a value that lives elsewhere.
  • AGENTS.md doesn't surface app-authored breaks: alongside depends/conflicts (agent-native): the new section thoroughly documents auto-injection but only mentions app-declared breaks as a parenthetical. A one-line addition would close the discoverability gap for someone reading AGENTS.md alone.
  • No integration test that asserts the rendered control file passes dpkg-buildpackage (testing, api-contract): the renderer test does substring containment, not paragraph-structure verification. A subtle template regression (e.g., an accidental blank line splitting the Debian paragraph) would silently drop the Breaks: relationship. Worth a follow-up CI smoke-test once we hit it once; not blocking.

Acknowledged residual risks (not blocking)

  • The two minimum-version constants are workspace-internal procedural knowledge. There is no cross-repo CI check that pins them to actually-released boundaries. Documented in AGENTS.md; flagged for future tightening.
  • Downstream halos-marine-containers rebuilds against CPT v0.6.0 will get the new Breaks: line silently — no metadata.yaml change required. This is exactly the intended behavior, but worth surfacing in the v0.6.0 release notes prominently (not only AGENTS.md).
  • Workspace docs/solutions/ doesn't yet document Debian Breaks: semantics (composition with Depends, apt-resolver behaviour, Breaks vs Conflicts). After this lands, worth a ce:compound doc filling that gap — the 2026-04-30 Skip-Depends-pins doc names the condition this PR addresses but doesn't yet point to Breaks as the right tool.

What's working well

Consolidated from all reviewers — useful to anchor what not to touch:

  • New breaks Pydantic field mirrors the shape of depends/recommends/conflicts/provides exactly. Backward-compatible (optional, defaults to None). Composition with app-declared breaks is correct and tested.
  • Module-level constant docstrings explain the failure mode that motivates the minimum, not just the value — exactly what a future maintainer needs when deciding whether to bump.
  • Tests reference the constants by name (HOMARR_ADAPTER_MIN_VERSION) rather than hardcoding "0.4.6". Bumping a constant doesn't require simultaneous test edits. The renderer test (test_rendered_control_file_breaks_for_routed_visible_app) does hardcode the literal version string — correct defense against the imported-constant tautology.
  • _compute_homarr_stack_breaks is a pure function of metadata, trivially testable in isolation.
  • 0.5.8 → 0.6.0 (minor) is the correct semver bump. The prior 0.5.7 → 0.5.8 path-only-URL feature should arguably have been a minor too; this PR resets the convention.
  • AGENTS.md note is well-placed (between Project Purpose and Project Status), at the right level of detail, and links back to the canonical workspace policy doc.
  • project-standards reviewer: full pass on AGENTS.md/CLAUDE.md compliance. No findings.
  • Plan doc placement in this repo (not the workspace) is correct — it's a CPT-specific concern. The plan explicitly states "Target repo: container-packaging-tools" up front.

Mode

Interactive read-only review per the human partner's request — no autofixes applied, no GitHub approval action, no merge. Findings posted as a single comment for the human to triage.

CE review (PR #203) flagged a predicate drift between the path-only-URL
emission in registry.generate_registry_toml and the Breaks-injection
trigger in template_context._compute_homarr_stack_breaks. The original
injection trigger required `routing AND web_ui.enabled AND web_ui.visible`,
but the path-only URL is actually emitted whenever `routing AND
web_ui.enabled` — visible is written *into* the TOML, it doesn't gate
emission.

Concrete consequence before this fix: a routed, enabled, non-visible
web app ships a path-only TOML with no Breaks: line. On an older
homarr-container-adapter the TOML entry silently fails to load, breaking
ping coverage for that app.

Fix: extract a single shared predicate `registry.emits_path_only_url`
consumed by both call sites. The two cannot drift, and the trigger
correctly fires for all path-only emissions (including non-visible
routed apps). The original test asserting visible=false suppresses
breaks is flipped to assert it injects breaks (since the path-only
TOML is still emitted). Also adds an explicit test for the
`routing: {}` case to lock in the `is None` semantics against future
schema defaults.

AGENTS.md updated to describe the correct trigger and to surface
app-authored `breaks:` as a peer of `depends`/`conflicts`. Plan doc
fixed to attribute the cross-format-identity learning to its actual
repo (homarr-container-adapter) rather than the workspace.
@mairas
Copy link
Copy Markdown
Contributor Author

mairas commented May 13, 2026

Pushed 0fe41e6 refactor: extract emits_path_only_url predicate to fix trigger drift addressing the P1 finding (and tests for the P2 routing-empty-dict gap, plus the plan-doc citation fix).

What changed

registry.emits_path_only_url(metadata) is now the single source of truth for the predicate. Both registry.generate_registry_toml's routed-URL branch and template_context._compute_homarr_stack_breaks call it. The predicate is metadata['routing'] is not None and metadata['web_ui']['enabled']web_ui.visible no longer gates Breaks injection (it never gated path-only TOML emission).

Test impact

  • Renamed test_web_ui_not_visible_no_breakstest_web_ui_not_visible_still_gets_breaks and flipped the assertion. The docstring explains why: a routed, enabled, hidden app still ships a path-only TOML and is loaded by the adapter for ping coverage, so the peer-version constraints apply.
  • Renamed test_routed_visible_app_gets_homarr_stack_breakstest_routed_enabled_app_gets_homarr_stack_breaks to match the new trigger.
  • Added test_routing_empty_dict_still_triggers_breaks to lock in the is None semantics (a future schema default of {} for routing would silently change the trigger if we relied on truthiness instead).

Polish also addressed

  • Plan doc citation fix (P2): the cross-format-identity-helper learning is attributed to homarr-container-adapter's own docs/solutions/, not the halos-org/halos workspace where it doesn't exist.
  • AGENTS.md note rewritten to describe the correct trigger and surface the app-authored breaks: field as a peer of depends/conflicts/provides for discoverability.

Polish deferred

These optional P3 items from the review are left for a follow-up if/when the constants need a bump:

  • Rename HOMARR_ADAPTER_MIN_VERSIONHOMARR_CONTAINER_ADAPTER_MIN_VERSION for naming parallelism with HALOS_CORE_CONTAINERS_MIN_VERSION.
  • Replace the live version string in the Pydantic breaks field's description example with a non-Homarr placeholder so it doesn't silently drift on a constant bump.
  • A future CI smoke-test that builds one .deb and asserts the rendered control file has the expected Breaks: line.

Local checks

578 unit + 27 integration pass; lint, format, and typecheck unchanged (same 2 pre-existing diagnostics).

@mairas mairas merged commit 5dcbe97 into main May 13, 2026
4 checks passed
@mairas mairas deleted the feat/inject-homarr-stack-breaks branch May 13, 2026 16:13
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