Skip to content

feat: read core flatpaks from the image label, and preview them in the wizard (ADR-0018) - #111

Merged
bsherman merged 4 commits into
mainfrom
benjamin/feat/core-flatpaks-label
Oct 7, 2026
Merged

bsherman merged 4 commits into
mainfrom
benjamin/feat/core-flatpaks-label

Conversation

@bsherman

@bsherman bsherman commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements ADR-0018 (now Accepted): all of roadmap Phase 10's firn work (steps 3 and 4), install path and wizard together.

With core_flatpaks = true, firn installed first-setup's GNOME list for every image. It read the list from the deployment, or, on composefs images (all snosi desktops), from the ISO's copy at /usr/share/firn/core-flatpaks.json. Sundog got GNOME apps, and Floe would have got a desktop set.

Firn now reads the org.frostyard.core-flatpaks label that each image publishes (frostyard/snosi#1054). It reads the label in preflight-image, from the same inspection that selects the image to install, so:

  • the set describes the image being deployed;
  • a malformed label stops the install before any disk write.

Release note: saved Sundog and Floe recipes with core_flatpaks = true now install that image's set, or none with a no_core_set warning, instead of the GNOME list. A locally seeded ISO still copies its whole seed (offline seeding is out of scope).

Do not tag a release from this until the published snosi images carry the label. The tag reaches the ISO immediately (ADR-0017). snosi#1054's main image build is running now.

Changes

  • internal/bootcimg/verify.go: CheckAndPinImage returns Source{Ref, Labels}. The labels come from whichever inspection selected the image:

    • the local copy when it wins, otherwise the registry image;
    • for an @sha256: ref, the local copy only if its digest matches.

    The selection logic itself is unchanged.

  • internal/flatpak/label.go (new): ParseCoreLabel implements the ADR's table fail-closed:

    • strict decoding that rejects unknown fields and trailing data;
    • version must be the integer 1;
    • Flatpak app-ID syntax, and a non-empty name;
    • an empty array means no core set;
    • duplicates dropped, keeping the first.
  • internal/flatpak/flatpak.go: removes CoreSet, InstallerCoreSet, coreJSONPath, InstallerCoreJSONPath and parseCoreJSON.

  • internal/steps/preflight.go: with core_flatpaks = true, parses the label after selecting the image:

    • a malformed label fails with the new error code core_flatpaks_label_invalid, in dry-run and real runs alike;
    • no core set emits no_core_set (warning plus summary item) and the install continues;
    • a valid set is kept in the new Env.CoreFlatpaks.

    The label is never parsed when core_flatpaks = false.

  • internal/steps/bootc.go: runFlatpaks installs the explicit apps, then the core set, each ID once. It no longer reads the deployment or the installer medium.

  • internal/progress, internal/pipeline: the new code constant and the CoreFlatpaks field.

  • Docs:

    • ADR-0018 Accepted;
    • ADR-0006's status notes that its core-set source is superseded;
    • the new docs/specs/core-flatpaks-label.md, indexed;
    • the core_flatpaks row in recipe-schema.md;
    • progress-protocol.md adds core_flatpaks_label_invalid and updates no_core_set;
    • architecture.md;
    • roadmap Phase 10 steps 1–3 marked done, and the stale Phase 9 Sundog note fixed (Sundog is offered through snosi's /etc/firn/catalog.json).
  • Wizard (internal/tui/core_preview.go, wizard_pages.go, wizard.go): the flatpaks page inspects the chosen image when it opens (30s timeout), using preflight's local-first selection without signature verification. It inspects again whenever the chosen image changes, including after "Start over". Four states:

    • valid set: the toggle, with the app names listed;
    • no set: a "No core app set" note instead of the toggle;
    • malformed label: a "Core app set unavailable" note with the reason, instead of the toggle;
    • inspect failed: the toggle stays, with a note that the list is read at install time.

    Hiding the toggle clears core_flatpaks. The preview never enters the recipe. flatpak.ParseCoreLabelApps keeps display names for the preview, and ParseCoreLabel wraps it.

  • test/e2e-tui.sh: the driver no longer accepts a core-app toggle for Floe. It waits for "publishes no core Flatpak apps" and fails if the toggle appears. The guest already pulls Floe from GHCR, so this adds no new registry dependency.

  • Docs: label spec rule 7 (the wizard preview), the recipe-schema TUI note, the architecture wizard paragraph, and roadmap Phase 10 step 4.

Review fixes (Codex)

  • Unreadable labels no longer pass as absent. Source.Inspected is true only when the labels came from a successful, parsed inspection of the selected image. A signed @sha256: ref can verify while its registry inspection failed and no matching local copy exists. With core_flatpaks = true, preflight now stops ("could not inspect image …") instead of reporting no_core_set and installing without the requested set. The wizard treats such a result as "inspection failed", keeps the toggle, and retries on the next visit.
  • Field names are case-sensitive. encoding/json matched VERSION, FLATPAKS, ID and NAME case-insensitively despite DisallowUnknownFields. The parser now checks exact field names at both object levels, matching the spec and snosi's producer.
  • Tests: four case-variant rows in TestParseCoreLabel; Inspected asserted in every label-selection row, including the offline pinned digest and an unparseable registry inspection; TestCoreFlatpaksUninspectableImageFailsPreflight (mutation-checked: it fails with the guard removed); TestCorePreviewUnreadableInspectionKeepsToggle. Spec rules 1, 3 and 7 updated.

Docs housekeeping

Verification

  • make check (fmt, lint, test) green
  • make verify green
  • New or changed behavior has focused tests, including failure paths; pipeline steps are covered through the runner fake
  • Wizard-page or nested-VM changes: make e2e-tui run locally against the re-synced driver: PASS (Floe installed in a nested VM, disk booted, hostname and user verified over SSH, generated recipe validated, with no core_flatpaks key)

New tests:

  • TestParseCoreLabel: 32 rows covering every ADR table case plus snosi's producer rejection cases.
  • TestCheckAndPinImageLabelsFollowSelectedImage: unsigned and signed, local vs registry, a pinned digest with a matching or mismatched local copy, offline, and an inspection with no labels.
  • TUI: TestCorePreviewStates (each state, plus caching per ref and retrying after a failure), TestCorePreviewFollowsImageChangesAndClearsHiddenToggle (Snow → Floe → malformed → Sundog), TestFlatpaksFormRendersEachPreviewState, TestFlatpaksFormWithoutCoreSetCompletes.
  • Step tests:
    • a malformed label fails preflight-image with core_flatpaks_label_invalid, with no destructive command, in dry-run and real runs;
    • the label is ignored when core_flatpaks = false;
    • no label or an empty set gives the warning and summary item;
    • a valid set is stored;
    • the explicit-then-core merge is deduplicated.
make check:  exit 0, golangci-lint 0 issues, 15 packages ok
make verify: exit 0
go test -race ./internal/{steps,bootcimg,flatpak,pipeline,tui}: ok
sudo test/e2e-tui.sh: e2e-tui: PASS — the wizard installed bootc floe inside a VM, the disk boots, and the generated recipe validates

Risk tier

  • low
  • moderate
  • high
  • critical

Rationale:

  • It changes what core_flatpaks installs and adds a new preflight failure, but touches no protected path (.github/workflows/**, .goreleaser.yaml, internal/install/**, cmd/**). Image selection logic is unchanged; only its return type grew labels.

🤖 Generated with Claude Code

With core_flatpaks, firn installed first-setup's GNOME list for every
image, read from an ISO copy because composefs hides the deployment's
/usr. Sundog got GNOME apps and Floe would have got a desktop set. Firn
now reads each image's org.frostyard.core-flatpaks label from the same
inspection preflight uses to select the source, so the set describes the
image being installed and a malformed label stops the install before any
disk write.

Saved Sundog and Floe recipes with core_flatpaks = true now install that
image's set, or none with a no_core_set warning, instead of the GNOME
list. A locally seeded ISO still copies its whole seed. Do not tag a
release until the published snosi images carry the label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The wizard offered the core-app toggle for every image without saying
what it would install, including Floe, which publishes no set. The
flatpaks page now inspects the chosen image's label with preflight's
local-first selection: it lists a published set by name, explains
instead of offering the toggle when an image has no set or a malformed
one, and keeps the toggle when inspection fails, since preflight reads
the label again at install. Hiding the toggle clears it, so a recipe
never asks for a set the wizard did not offer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bsherman bsherman changed the title feat(flatpak): read core flatpaks from the image label (ADR-0018) feat: read core flatpaks from the image label, and preview them in the wizard (ADR-0018) Oct 6, 2026
A signed digest can pass cosign verification while its registry
inspection failed and no matching local copy exists. Its labels were then
treated as absent, so a core_flatpaks install reported no_core_set and
skipped the requested apps, and label validation never ran. Source now
records whether its labels came from a successful inspection, and
preflight stops when core_flatpaks needs labels it could not read.

encoding/json matches field names case-insensitively, so the parser
accepted VERSION, FLATPAKS, ID and NAME despite DisallowUnknownFields,
unlike the spec and snosi's producer. Field names are now checked exactly
at both object levels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bsherman
bsherman enabled auto-merge October 6, 2026 21:06

@bketelsen bketelsen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks good. The selection logic in CheckAndPinImage is unchanged: the new branches only decide where the labels come from. The parser matches the spec table and snosi's check_set, and failing closed when Inspected == false is the right call. go test passes for flatpak, bootcimg, steps and tui at ed55c0d.

Release gate: the published :latest images now carry the label. I just ran skopeo inspect: snow and snowfield have 23 apps, sundog has 10, and floe has no key. All four parse cleanly with this branch's ParseCoreLabelApps, so the "do not tag until…" condition looks met for latest.

Inline comments, roughly by priority:

  1. While it inspects, the wizard blocks for up to 30 s with nothing on screen (wizard.go).
  2. The two inspections share one timeout, so a hanging registry starves the local inspect (core_preview.go).
  3. Display names accept control characters and terminal escapes, and they render before verification (label.go).
  4. {"version":1} gets a misleading error message (label.go).
  5. Nits: wrapping counts bytes, not characters; the uninspectable case has no stable error code.

One doc nit: under Fail before writing, ADR-0018 says only headless recipes can reach core_flatpaks_label_invalid. A wizard recipe can reach it too: when the preview's inspection failed and the toggle stayed, or when the tag moved between the preview and the install. Worth correcting now, while the ADR is still being edited in this PR.

Review drafted with Claude Code.

Comment thread internal/tui/wizard.go Outdated
Comment thread internal/tui/core_preview.go
Comment thread internal/flatpak/label.go
Comment thread internal/flatpak/label.go Outdated
Comment thread internal/tui/core_preview.go Outdated
Comment thread internal/steps/preflight.go Outdated
The flatpaks page inspected the image with nothing on screen for up to
30 seconds on a network that drops packets, and keys pressed meanwhile
reached the form. A spinner now shows while it inspects and swallows
those keys. The registry inspect ran first and shared that deadline, so
a hanging registry also hid an image already present locally;
CheckAndPinImage now inspects the local copy first.

Names render on the installer console before the image is verified, so
names with control or format characters are malformed. A label with
version but no flatpaks reported a JSON error instead of the spec's
message, preview wrapping counted bytes rather than display width, and
unreadable labels failed preflight without a stable code; they now have
core_flatpaks_label_unreadable. ADR-0018 no longer claims only headless
recipes can reach a label error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bsherman

bsherman commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, all addressed in 351ae37 (inline replies above). The ADR-0018 Fail before writing bullet now says a wizard recipe can also reach the label errors: when the preview's inspection failed and the toggle stayed, or when the tag moved between preview and install.

Validation at 351ae37: make check and make verify green, go test -race ./internal/... clean, and make e2e-tui PASS locally (Floe through the new spinner and the no-core-set note, disk booted, recipe validated). The producer-side printable-name rule is frostyard/snosi#1055.

@bsherman
bsherman added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 926e9e3 Oct 7, 2026
5 checks passed
@bsherman
bsherman deleted the benjamin/feat/core-flatpaks-label branch October 7, 2026 02:36
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.

2 participants