diff --git a/.github/actions/folio-app/action.yml b/.github/actions/folio-app/action.yml index 5f105ad..5ef98f9 100644 --- a/.github/actions/folio-app/action.yml +++ b/.github/actions/folio-app/action.yml @@ -46,6 +46,7 @@ runs: # it broke the staging and made the embedded version string a lie. Moving to # 1.5.0 is a companion change, not a CI one. - name: Install idb-companion, xcodegen and just + id: idb if: inputs.platform == 'ios' shell: bash env: @@ -55,19 +56,38 @@ runs: brew tap facebook/fb git -C "$(brew --repository facebook/fb)" checkout --quiet c0386793f59da10c619787f2aa18d938ef1d69c9 brew install facebook/fb/idb-companion + echo "companion-version=$(brew list --versions idb-companion | awk '{print $2}')" >> "$GITHUB_OUTPUT" # Both asset tarballs are built by the prepare scripts, and the runner # bundle is an xcodebuild of companion/Sources. Keyed on the scripts and # the versions the Makefile embeds, so a later run reuses them. This has to # land before `make sanderling-ios`, which is what consumes them. + # + # The formula version is in the key because it is the companion tarball's + # largest input and it lives outside the repository: prepare.sh copies + # whatever brew installed. Without it, a tap pin bumped on its own would hit + # a cache filled from the old formula and embed it under the new pin. - name: Cache the companion and runner bundles + id: ios-assets if: inputs.platform == 'ios' uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | internal/driver/ioscompanion/companionassets/assets internal/driver/ioscompanion/runnerassets/assets - key: ios-assets-${{ runner.os }}-${{ hashFiles('internal/driver/ioscompanion/companionassets/prepare.sh', 'companion/prepare.sh', 'companion/project.yml', 'companion/Sources/**') }} + key: ios-assets-${{ runner.os }}-idb${{ steps.idb.outputs.companion-version }}-${{ hashFiles('internal/driver/ioscompanion/companionassets/prepare.sh', 'companion/prepare.sh', 'companion/project.yml', 'companion/Sources/**') }} + + # A restored tarball keeps the mtime it was archived with, while the + # checkout stamps the prepare scripts and companion sources with checkout + # time, so make reads every restored bundle as older than its sources and + # rebuilds it. The key covers all of those sources exactly, so a hit means + # the bundles match them and the timestamps are the only thing lying. + - name: Date the restored bundles after their sources + if: inputs.platform == 'ios' && steps.ios-assets.outputs.cache-hit == 'true' + shell: bash + run: | + touch -c internal/driver/ioscompanion/companionassets/assets/*.tar.gz \ + internal/driver/ioscompanion/runnerassets/assets/*.tar.gz # Without this the emulator falls back to software rendering and every # step costs several seconds. diff --git a/docs/development/ci.md b/docs/development/ci.md index 4f3d6fb..698c57b 100644 --- a/docs/development/ci.md +++ b/docs/development/ci.md @@ -346,7 +346,7 @@ The job installs npm 11.5.1 or newer before publishing, because `actions/setup-node` writes an empty `_authToken` line into `.npmrc` and an older npm reads that as "auth is configured" and never asks for an OIDC token. -## The ios companion is pinned, and its cache does not save the build +## The ios companion is pinned, and its cache now saves the build `.github/actions/folio-app/action.yml` checks the `facebook/fb` tap out at commit `c0386793`, the 1.1.8 formula, before installing `idb-companion`. Floating on the @@ -358,13 +358,28 @@ top-level `Frameworks/`, and `companionassets/prepare.sh` stages `bin/` and called 1.1.8 out of whatever the tap was serving that day. Moving to 1.5.0 is a change to the companion, not to CI. -The `ios-assets` cache does not protect against this, and it is worth knowing why -before trusting it. It restores, and the build runs anyway: git stamps the -checked-out `prepare.sh` with checkout time while the restored tarball keeps the -mtime it was archived with, so make always reads the target as stale. The -2026-08-18 failure logged `Cache hit` and `Cache restored successfully`, then ran -`prepare.sh` and died. So every ios run rebuilds the companion from whatever -`brew` just installed, and a green master says nothing about the tap. +The `ios-assets` cache had to be taught to hold. It restored, and the build ran +anyway: git stamps the checked-out `prepare.sh` with checkout time while the +restored tarball keeps the mtime it was archived with, so make read the target as +older than its prerequisite every time and rebuilt it. The 2026-08-18 failure +logged `Cache hit` and `Cache restored successfully`, then ran `prepare.sh` and +died copying the `Frameworks/` that 1.5.0 does not have. + +A step after the restore now touches both restored tarballs, which dates them +after the sources the checkout stamped, and make leaves them alone. That fix sits +with the cache rather than in the Makefile because the cache is what makes the +timestamps lie: the key hashes both prepare scripts, `companion/project.yml` and +`companion/Sources/**`, which is exactly the prerequisite set of the two make +rules, and the step carries no `restore-keys`, so a hit is an exact match on all +of them. Declaring those prerequisites order-only would have fixed CI and broken +local work, where someone editing `prepare.sh` has no cache key to catch the +edit and make would quietly keep the previous tarball. + +The key also carries the installed formula version, read from `brew list` right +after the install. The companion tarball's largest input is the homebrew install +itself, which no hash of the repository can see, so without it a bump of the tap +pin on its own would hit a cache filled from the old formula and embed that under +the new pin, which is the lie the pin exists to stop. Master looked green through the breakage only because its last run predated the tap moving, not because anything shielded it.