feat(scripts): refresh a vendored host tree from its source repository - #811
Conversation
The host trees are snapshots and the repositories they came from keep moving, so the refresh runs once per host at cutover and then again for as long as a source repository keeps a release line of its own. It was a procedure nobody had written down, recoverable only from two pull request descriptions. The two ways it goes wrong have both already happened. Replacing the tree wholesale discards this repository's adaptations, and re-applying adaptations without replacing the tree discards upstream's work, so the new tree is taken whole and the adaptations go back on top as a three-way patch. Separately, `git add` honours ignore rules the source repository never had: an earlier import lost 57 files that way and only a hash comparison caught it, so paths are staged with --force and every one is compared against the source by blob hash. Differences are checked against the adaptation set rather than counted. One that nothing accounts for is upstream work the refresh dropped, and is reported as unexplained. hosts/imports.json records the source and revision per host, so each refresh starts from what was actually imported rather than from an archaeology exercise, and is updated as part of the run. The recorded revisions are the ones the trees on this branch actually match: Android differs from its source only by the .gitignore adapted at import, iOS by the fourteen workflows moved to the root and the manifest and signing adaptations. Listings are NUL-delimited. git quotes any path outside ASCII, and one iOS asset has a Cyrillic character, which read as both missing and extra when the directory prefix was pasted onto an already-quoted path.
|
CI Status: 18 required jobs green, 12 passed and 6 skipped by path filter. All job results
Commit |
|
@lore-bot-app review |
|
Reading the diff and checking what the record says. Back in a few minutes. |
There was a problem hiding this comment.
TL;DR
Adds scripts/refresh-host-import.sh plus hosts/imports.json so hosts/ios and hosts/android can be re-imported from upstream with this repo's adaptations re-applied as a three-way patch. 3 blocking, 4 minor. As written, refresh aborts on its first git call, so the refresh path cannot have run end to end.
Summary
The script records each vendored host's source repo, branch and revision in hosts/imports.json. status fetches the recorded revision and compares every tracked path under hosts/<host> against it by blob hash. refresh builds the recorded upstream tree under the hosts/<host>/ prefix, diffs it against HEAD to extract this repo's adaptations, replaces the tree with the new upstream revision via git archive, force-stages it, re-applies the adaptations with git apply --3way, compares the result against upstream, and updates the manifest. A Python helper does the NUL-delimited comparison and fails when a difference has no adaptation behind it. README and CLAUDE.md gain layout entries and a usage section.
What the record says
- Issue #726 prescribes this exact procedure: merge rather than tree replace, three-way patch, blob-hash verification, confirm no files dropped by ignore rules. It was rehearsed by hand with 161 commits of drift. This PR is the automation of that.
- PR #703 is the 57-file incident. The rules were
token*and*.jsin the vendored tree's ownhosts/android/.gitignore, fixed by anchoring them. The README's "this repository's ignore rules are not the source's" is slightly off: they were the source's rules, which in the source never applied because the files were already tracked. - PR #774 refreshed
hosts/iosby 200+ commits by hand, with conflicts in the network suffix logic andPackage.resolvedresolved manually. It is not on this branch. See question 1. - PR #690 imported iOS at
develop844e1b9, and PR #703 imported Android. Both match the manifest. - PR #787 sets the iOS cutover for Thursday 17 September, so this script is expected to run tomorrow.
docs/design/host-monorepo.md:173says the import commit is where the source revision is recorded. The manifest is now a second record and the design doc does not mention it.- Tarik Gul owns this area per
who_knows; they are the author.
Concerns
Blocking:
scripts/refresh-host-import.sh:82-83.mktempcreates a 0-byte file, andgit read-treewithGIT_INDEX_FILEpointing at an existing empty file dies with "index file smaller than expected". Git only tolerates a missing index file. Withset -eeveryrefreshstops here before touching the tree. Fix:rm -f "$index"before line 83, ormktemp -u. I could not execute in this environment. Check with:GIT_INDEX_FILE=$(mktemp) git read-tree --prefix=x/ HEADscripts/refresh-host-import.sh:198. After a conflicted--3wayapply,git add --all --forceclears the unmerged index entries and stages the conflict markers as content.git statusthen shows ordinary staged changes,git commitaccepts them, andscripts/lib/compare-host-import.py:81counts the path as explained because it is in the adapted list. The README's "left staged, a conflict is a human decision" does not hold. Fix: run line 198 only when apply succeeded, or excludegit diff --name-only --diff-filter=Upaths and print them.scripts/refresh-host-import.sh:193-197withscripts/lib/compare-host-import.py:81.git applyis all-or-nothing when any single patch is rejected outright rather than sent to 3-way: a binary hunk from a diff made without--binary, or an adapted path that upstream has since deleted or renamed ("does not exist in index"). Nothing is applied, line 196 says "conflicts left", the tree equals upstream, and the comparison prints "every difference is accounted for" because the check is one-sided. It flags a difference with no adaptation but never an adaptation with no difference. The current iOS adaptation set has 32 paths, none binary and none renamed, measured from this checkout's root, so it is latent today. One upstream rename of a workflow file we delete triggers it. Fixes:git diff --binaryat line 177, apply per adapted path so one rejection does not drop the rest, and report adapted paths whose blob now equals upstream.
Minor:
hosts/imports.json:4,9. Abbreviated SHAs are not valid fetch refspecs, so line 71 always fails and line 72 fetches the remote HEAD wholesale. That works only while the recorded commit is reachable from the remote's default branch. A force-push or a non-default branch makesrefreshdie at line 75.manifest_set_refwrites full SHAs, so formats mix after the first run. Record full SHAs now.scripts/refresh-host-import.sh:186.rm -rfdeletes ignored developer files:hosts/android/local.properties(hosts/android/.gitignore:10),hosts/ios/source_packagesand.build/(hosts/ios/.gitignore:35-36). The dirty check at line 154 does not see ignored files.git rm -r -q hosts/<host>thengit read-tree --prefix=hosts/<host>/ -u <target>^{tree}replaces lines 186-189, touches only tracked files, bypasses ignore rules without--force, and avoidsgit archive, which would drop any future upstreamexport-ignorepath (none today). AGENTS.md: leave unrelated local changes alone.README.md:265-288. The new section sits between the### Working on the iOS hostheading and its body. That heading is now empty and the iOS build steps appear under "Refreshing a vendored host tree". Also pointdocs/design/host-monorepo.md:173at the manifest.scripts/refresh-host-import.sh:10-20andREADME.md:280-282narrate history ("both have already happened", the 57 files). CLAUDE.md: comments describe current code only, no migration or history narration. #703 already holds the story. One line stating the rule is enough.
Questions for the author
- #774 moves
hosts/iosabout 200 commits past 844e1b99 and is not on this branch. Which PR updates the iOS ref? If #774 lands first, the nextrefreshtreats those 200 upstream commits as adaptations. compare-host-import.pyis the first Python file inscripts/lib/. Every other helper is.mjswith a sibling test thatnpm run test:scriptsruns (package.json:17). Is Python deliberate, and where does its test live?- Has
refreshrun end to end against real drift, for example replaying #774's range? The commit message's "which files differ" claim is obtainable fromstatusalone.
Next: run the one-liner in concern 1.
🤖 Reviewed by Lore (Parity knowledge base) · 47 agent turns · 633.0s · knowledge as of 2026-09-16
|
All seven addressed in 6fdac54. Three were real breakage and I had not caught The empty index. Confirmed exactly as described: Staging a conflicted apply. Correct, and it defeated the whole claim. Staging All or nothing apply. The patch is made with
Abbreviated SHAs. Both now full. The orphaned heading. Correct, the section split History narration. Removed from the header and the README. The rule is A real Not done: per-path apply so one rejection cannot drop the rest. The outright |
|
@lore-bot-app review |
|
Reading the diff and checking what the record says. Back in a few minutes. |
There was a problem hiding this comment.
Since last review
- Resolved: empty-index crash (
rm -f "$index"atscripts/refresh-host-import.sh:84), staging after a conflicted apply (now only on success,:193-195),--binaryon the diff (:178), full SHAs inhosts/imports.json,git rm+read-treeinstead ofrm -rf(:188-189), README section placement, design doc pointer, trimmed history narration. - Partly resolved: all-or-nothing
git apply. The outright-rejection case is now detected and dies (:201-205), and inert adaptations are reported (compare-host-import.py:86-91). There is still no way to complete a refresh once one adapted path was deleted or renamed upstream. - Still open: iOS ref vs #774, Python helper without a test.
- New: one blocking problem introduced by the
rm -rffix, plus two minor ones.
TL;DR
Automates the host-tree refresh from #726 with a manifest, a three-way re-apply and a blob-hash comparison. 1 blocking, 3 minor.
Summary
hosts/imports.json records source, branch and revision per vendored host. status fetches the recorded revision and compares every tracked path by blob hash. refresh builds the recorded upstream tree under hosts/<host>/, diffs it against HEAD to extract this repo's adaptations, replaces the tree with the target revision via git rm and git read-tree -u, re-applies the adaptations with git apply --3way, compares the result against the target in both directions, and records the new revision. Conflicts are left unmerged. A patch rejected outright aborts.
What the record says
- Issue #726 prescribes exactly this procedure: three-way patch rather than tree replace, blob-hash verification, and a check that no file is dropped by ignore rules. This PR is that automation.
- PR #811 (this PR) records an end-to-end
refreshofhosts/androidwith 498 files staged and no missing or extra paths against the source. That answers the earlier "has it run" question for Android. No iOS run is recorded. - PR #774 refreshed
hosts/iosby hand past 844e1b99 and is described as complete with CI passing. It is not in this branch:git log -- hosts/ioshere ends at #706, and no commit references #774. Whichever lands second must update the iOSref, or the nextrefreshtreats 200 upstream commits as adaptations. - PR #703 is the 57-file drop caused by the vendored
hosts/android/.gitignore. Theread-treepath now sidesteps it for tracked content, which is the right fix. The--forcethat the earlier revision used to work around it is what causes the concern below. - Tarik Gul owns this area per
who_knowsand is the author.
Concerns
Blocking:
scripts/refresh-host-import.sh:195.git add --all --force hosts/<host>after a clean apply stages every ignored file under the tree.--forceexists to add otherwise-ignored files, andgit apply --3wayalready updated the index, so the line adds nothing tracked. What it does add is whatever the developer has locally:hosts/ios/.build/andsource_packages(hosts/ios/.gitignore:35-36),hosts/android/.gradle/andbuild/(hosts/android/.gitignore:2-4), and the secret patternspolkadot-app/env-vars.sh,GoogleService-Info.plist(hosts/ios/.gitignore:89-91),/token*,.env*(hosts/android/.gitignore:56-57). The comparison lists them under "here but not in the source" but does not fail, and the script's last line still says "review it, then commit". In a public repo that is a credential leak onegit commitaway. Fix: delete line 195. The recorded Android run saw no extras because that checkout had no ignored files underhosts/androidat the time.
Minor:
-
scripts/refresh-host-import.sh:193,205. Detecting the outright rejection is an improvement, but a single adapted path that upstream deleted or renamed, or a pure-rename adaptation (3-way declines renames with no content change), now makesrefreshunusable for that host with no knob to continue. The die also leaves the plain upstream tree staged in the index with no recovery hint. At minimum printgit reset --hard HEADin the message. A--skip <path>option, or applying the patch per file so one rejection does not block the rest, would let the operator finish. -
scripts/refresh-host-import.sh:214. The comparison's exit status is discarded and the final lines always say the refresh is ready for review. The "inert adaptation" and "UNEXPLAINED" verdicts scroll past in the middle. Either propagate the status or print the verdict after the manifest note, so the last line of output is the one that matters. -
scripts/lib/compare-host-import.py. Still the only Python underscripts/lib/and the only helper there without a test, whilenpm run test:scriptscovers every.mjssibling (package.json:17,ci.yml:482). The inert and unexplained branches are the logic the whole procedure relies on and are cheap to cover with two fixture files.
Questions for the author
- Which PR updates the iOS
refinhosts/imports.jsonafter #774 lands, and to which upstream commit? - Of the iOS adaptation paths, has any been deleted or renamed on upstream
developsince 844e1b99? If so, the first iOSrefreshhits concern 2 immediately. - Is Python deliberate for the comparison helper, given the
.mjsplus test convention inscripts/lib/?
Next: delete scripts/refresh-host-import.sh:195 and re-run the Android refresh with an ignored file placed under hosts/android/ to confirm it stays unstaged.
🤖 Reviewed by Lore (Parity knowledge base) · 36 agent turns · 415.5s · knowledge as of 2026-09-16 · re-review
The forced add after a clean apply swept in everything ignored under the host tree. On these hosts that includes GoogleService-Info.plist, env-vars.sh, token files and .env, so a refresh left credentials one commit from a public repository. It also was not doing any work: the three-way apply has already updated the index for every path it touched. An outright rejection now says how to get back, since it leaves the plain upstream tree staged. The comparison used to print in the middle of the output with its status discarded, so a refresh that dropped upstream work still ended with "review it, then commit". The verdict is last now, and a dropped path is fatal.
|
All three fixed. The forced add. You are right and it is the serious one. Verified rather than reasoned about: planted Recovery hint. The outright rejection message now ends with The verdict scrolling past. Moved after the manifest note and made fatal. A Not done: |
filvecchiato
left a comment
There was a problem hiding this comment.
approving to unblock, but small nit
The adaptation list came from `git diff --name-only`, which quotes and escapes any path outside ASCII, while the listings it is compared against are NUL delimited and raw. The one non-ASCII asset in the iOS tree therefore could never match its own entry, so the first refresh that adapted it would call it dropped work and die, after the tree had already been rewritten and staged. The list is NUL delimited now and read as records, which is the encoding the rest of the comparison already used.
|
Fixed, and it is a good catch: I moved the listings to Confirmed there is precisely one non-ASCII tracked path under Took the Verified by running the comparison against that path both ways: with the quoted |
The host trees under
hosts/are snapshots and their sources keep moving. Arefresh runs at cutover, then for as long as a source repository keeps a release
line of its own. It was a procedure nobody had written down.
Both ways it goes wrong have already happened, so both are encoded. A tree
replace drops work in one direction or the other, so the new tree is taken whole
and adaptations reapplied as a three-way patch.
git addhonours ignore rulesthe source never had, which cost an earlier import 57 files, so paths are staged
with
--forceand every one is compared against the source by blob hash. Adifference no adaptation accounts for is reported as unexplained, and conflicts
are left uncommitted.
hosts/imports.jsonrecords the source and revision per host.statusis whatconfirms the recorded ones are right: Android differs from its source only by the
.gitignoreadapted at import, iOS by the workflows moved to the root plus themanifest and signing adaptations.
Part of #726.