Skip to content

fix: read a subdir app's art from beside its manifest - #51

Merged
CrysisDeu merged 1 commit into
mainfrom
fix/publish-subdir-assets
Sep 24, 2026
Merged

CrysisDeu merged 1 commit into
mainfrom
fix/publish-subdir-assets

Conversation

@CrysisDeu

Copy link
Copy Markdown
Collaborator

Summary

For a git entry with source.subdir, tools/publish.py read app.json from the subdirectory but read every asset that manifest names (iconPath, iconPathDark, heroImage, heroImageDetail, screenshots[]) from the repository root. A monorepo app declaring iconPath: "icon.png" beside its app.json therefore published a card with no icon, hero or screenshots. This PR joins the entry's subdir into every asset read.

Cause

  • tools/publish.py:454-457 -- fetch_manifest(url, commit, subdir) joins subdir/app.json before git show.
  • tools/publish.py:924 -- fetch_blob(url, commit, path) runs git show commit:path with whatever it is handed.
  • tools/publish.py:1464-1468, :1546, :1553-1557 -- IconAssets.add, the hero/detail-hero ingester.add, and ScreenshotAssets.add all pass the bare manifest path.
  • origin.get("subdir") reached only the manifest read at :1629; no ingester ever saw it.

Fix

  • New contained_blob_path(subdir, rel_path) next to fetch_blob: joins subdir/<manifest path> with the same Path normalization fetch_manifest uses, and refuses an absolute result, a .. segment, a backslash or a NUL. Containment is re-stated at the point where the value becomes a git path rather than trusted from the two schemas upstream.
  • _FetchedImages.add(...) takes a keyword-only subdir=None and joins once, before the size probe and the read, so sizer and reader can never name two different blobs. Diagnostics keep naming the manifest's own spelling (rel_path).
  • bake_entry reads source.get("subdir") and passes subdir=subdir to the icon, hero, detail-hero and screenshot .add(...) calls.

Not changed: the meaning of iconPath (app-relative stays app-relative), validate.py, changed_entries.py, the schemas, the published source.subdir key. Root apps read the same bare path as before; builtin entries never reach an ingester and are byte-identical (pinned by a test).

Tests

22 new cases in tests/test_publish.py (TestSubdirAppsReadArtBesideTheirManifest, TestContainedBlobPath):

  • source.subdir="apps/beta" + iconPath="icon.png" hands apps/beta/icon.png to the reader; same for iconPathDark, heroImage, heroImageDetail, screenshots[], and the size probe.
  • An entry without subdir still passes the bare path; a builtin entry is byte-identical.
  • A subdir app whose asset is missing at the joined path reports the same cannot read icon 'icon.png' warning, no error, no raise.
  • Containment: 6 accept spellings, 7 reject spellings, and an uncontained join warns without reaching the reader.
  • The measured reproduction end to end through a real git show: two apps in one repo, only apps/beta/icon.png is admitted (not apps/alpha/icon.png, not a root icon.png).

Red-first: 20 of the 22 fail on main's publish.py (the two that pass are the root-app and builtin regression guards, by design). Mutation-verified: dropping subdir= from any one of the three call sites, feeding rel_path to the sizer or the reader, or deleting either containment check each fails at least one test. A redundant pre-check that no mutation could kill was removed.

One existing test (test_release_entry_ingests_art_from_the_repository_not_the_asset_url) had a hand-rolled SpyAssets.add with a fixed positional signature; it now accepts the subdir keyword.

Locally (Python 3.12): pytest tests/ -q 500 passed before, 522 passed after; python tools/validate.py OK (3 pre-existing warnings); python tools/format.py --check all 3 files normalized.

Related

The client-side twin of this defect was fixed in kirodotdev/KiroCrew#12903 (the store card joins subdirectory into every asset URL); with this change the two sides agree on what an app-relative path means.

Fixes #50

`fetch_manifest` joins `source.subdir` before reading `app.json`, but every
asset that manifest names -- `iconPath`, `iconPathDark`, `heroImage`,
`heroImageDetail`, `screenshots[]` -- was handed to `fetch_blob` with the
bare manifest path, so `git show` looked for a monorepo app's icon at the
repository root. A correct manifest declaring `iconPath: "icon.png"` beside
its `app.json` published a card with no icon, hero or screenshots.

Thread the entry's `subdir` into every `_FetchedImages.add(...)` call and
join it there, once, before the size probe and the read, so the sizer and
the reader can never name two different blobs. The join is contained at the
point where it becomes a git path (no absolute result, no `..` segment, no
backslash or NUL) rather than trusted from the schemas upstream. The
manifest key keeps its app-relative meaning -- the installed app and the
store client both resolve it that way -- and the published ref is still the
hosted, content-addressed path. Root apps read the same bare path as
before; builtin entries never reach an ingester and are unchanged.

Tests pin the path handed to the reader for every asset kind, the
unchanged root-app and builtin cases, the same warn-and-continue finding
for a subdir asset that is missing, containment of the join, and the
measured reproduction end to end through a real `git show`. Every case
fails on the previous `publish.py`; each `subdir=` call site and both
containment checks are mutation-verified.

The client-side twin of this defect was fixed in kirodotdev/KiroCrew#12903.

Fixes #50
@github-actions

Copy link
Copy Markdown

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 6523bc5639860dd3b6aac190a7216eff4c50f93c and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 6523bc5

@github-actions

Copy link
Copy Markdown

Opus 5 Review — ✅ no blocking findings

Opus 5 completed its review of 6523bc5639860dd3b6aac190a7216eff4c50f93c and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 6523bc5

@CrysisDeu
CrysisDeu merged commit 8555f03 into main Sep 24, 2026
6 checks passed
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.

publish.py reads subdir apps' assets (icon, hero, screenshots) from the repo root, not the subdir

1 participant