diff --git a/.github/actions/folio-app/action.yml b/.github/actions/folio-app/action.yml index f0368687..297a2269 100644 --- a/.github/actions/folio-app/action.yml +++ b/.github/actions/folio-app/action.yml @@ -10,7 +10,7 @@ runs: using: composite steps: - name: Set up the JDKs - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin # The metro gradle plugin folio builds with needs a 21 runtime; the @@ -28,7 +28,7 @@ runs: - name: Cache Gradle if: inputs.platform != 'ios' - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches @@ -50,7 +50,7 @@ runs: # land before `make sanderling-ios`, which is what consumes them. - name: Cache the companion and runner bundles if: inputs.platform == 'ios' - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | internal/driver/ioscompanion/companionassets/assets diff --git a/.github/actions/headless-chrome/action.yml b/.github/actions/headless-chrome/action.yml index 223df709..1211f404 100644 --- a/.github/actions/headless-chrome/action.yml +++ b/.github/actions/headless-chrome/action.yml @@ -8,7 +8,7 @@ runs: # the action cannot move the browser these jobs drive. The alternative it # offers is Chrome for Testing latest, which tracks ahead of the channel # users run. - - uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2 + - uses: browser-actions/setup-chrome@48ad923757ca74d66703209fe939badbdf80f2f4 # v2.2.0 with: chrome-version: stable diff --git a/.github/scripts/folio-run-test.sh b/.github/scripts/folio-run-test.sh index ef2344b6..7986211c 100755 --- a/.github/scripts/folio-run-test.sh +++ b/.github/scripts/folio-run-test.sh @@ -110,7 +110,7 @@ expect_argv "--seed" argv-ios expect_argv "7" argv-ios expect_argv "240" argv-ios expect_argv "20m" argv-ios -expect_argv "iPhone 16 Pro" argv-ios +expect_argv "iPhone 17 Pro" argv-ios run argv-android android 2 <> "$GITHUB_PATH" - name: Cache Gradle - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches @@ -115,7 +123,7 @@ jobs: # JAVA_HOME after `make test` rather than installing both up front # leaves every step above this one on exactly the JDK it ran on before. - name: Set up JDK 21 for folio - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin java-version: "21" @@ -127,10 +135,10 @@ jobs: name: Check (browser) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache: true @@ -145,7 +153,7 @@ jobs: name: Check (workflows) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Pinned so a new actionlint release cannot change what CI enforces, # for the same reason the buf version above is spelled out. shellcheck @@ -187,10 +195,10 @@ jobs: MAX_STEPS: "200" DURATION: 20m steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache: true @@ -198,7 +206,7 @@ jobs: - name: Set up bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: "1.3.13" + bun-version: "1.3.14" - name: Build the folio app uses: ./.github/actions/folio-app @@ -220,7 +228,7 @@ jobs: - name: Upload the run if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: folio-android path: runs/ @@ -228,18 +236,19 @@ jobs: folio-ios: name: Folio (ios) - runs-on: macos-15 + runs-on: macos-26 timeout-minutes: 90 env: SEED: "7" MAX_STEPS: "240" DURATION: 20m - IOS_DEVICE: iPhone 16 Pro + IOS_DEVICE: iPhone 17 Pro + IOS_RUNTIME: iOS 26.2 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache: true @@ -247,7 +256,7 @@ jobs: - name: Set up bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: "1.3.13" + bun-version: "1.3.14" - name: Build the folio app uses: ./.github/actions/folio-app @@ -257,10 +266,53 @@ jobs: - name: Build sanderling run: make sanderling-ios + # simctl resolves a device by name alone, and one image carries the same + # phone under several runtimes: iPhone 17 Pro exists here on iOS 26.2, + # 26.4 and 26.5. Booting by name is therefore booting on whichever one + # simctl happens to list first, and a seed only means something against a + # fixed runtime. So the pair is resolved to a UDID here and every step + # after this boots that one simulator. An image that stops carrying the + # pair fails here naming what it does carry, rather than as a + # `bootstatus` error to read backwards from. + # + # The UDID stays in this step. IOS_DEVICE has to keep holding the name, + # because `just ios` spends it as xcodebuild's `-destination name=`, which + # matches on the display name and rejects a UDID. Nothing downstream needs + # it anyway: the install, the launch and the terminate all address + # `booted`, and sanderling resolves --ios-device against booted simulators + # before available ones, so the one booted here is the one they all get. - name: Boot a simulator run: | - xcrun simctl boot "$IOS_DEVICE" || true - xcrun simctl bootstatus "$IOS_DEVICE" -b + udid="$(python3 <<'PY' + import json, os, subprocess, sys + + want_device = os.environ["IOS_DEVICE"] + want_runtime = os.environ["IOS_RUNTIME"] + devices = json.loads(subprocess.run( + ["xcrun", "simctl", "list", "devices", "available", "--json"], + capture_output=True, text=True, check=True).stdout)["devices"] + + def name_of(runtime): + family, _, version = runtime.rsplit(".", 1)[-1].partition("-") + return "%s %s" % (family, version.replace("-", ".")) + + for runtime, entries in devices.items(): + if name_of(runtime) != want_runtime: + continue + for entry in entries: + if entry["name"] == want_device: + print(entry["udid"]) + sys.exit(0) + + carried = sorted({"%s on %s" % (e["name"], name_of(r)) + for r, es in devices.items() for e in es}) + sys.exit("no %r on %r in this image. it carries:\n %s" + % (want_device, want_runtime, "\n ".join(carried) or "no simulators at all")) + PY + )" + echo "booting $IOS_DEVICE on $IOS_RUNTIME ($udid)" + xcrun simctl boot "$udid" + xcrun simctl bootstatus "$udid" -b - name: Build and install folio working-directory: examples/folio @@ -276,7 +328,7 @@ jobs: - name: Upload the run if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: folio-ios path: runs/ @@ -291,10 +343,10 @@ jobs: MAX_STEPS: "240" DURATION: 20m steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache: true @@ -302,7 +354,7 @@ jobs: - name: Set up bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: "1.3.13" + bun-version: "1.3.14" - name: Set up headless Chrome uses: ./.github/actions/headless-chrome @@ -320,7 +372,7 @@ jobs: - name: Upload the run if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: folio-web path: runs/ @@ -348,10 +400,10 @@ jobs: MAX_STEPS: "80" DURATION: 10m steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache: true @@ -359,7 +411,7 @@ jobs: - name: Set up bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: "1.3.13" + bun-version: "1.3.14" - name: Set up headless Chrome uses: ./.github/actions/headless-chrome @@ -441,7 +493,7 @@ jobs: - name: Upload the run if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: replay-ui-runs path: runs/ @@ -473,7 +525,7 @@ jobs: tag: ${{ steps.next.outputs.tag }} previous_tag: ${{ steps.next.outputs.previous_tag }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # The version is counted off the tags, so the tags have to be here. fetch-depth: 0 @@ -510,28 +562,27 @@ jobs: # caps a granular one at 90 days, so a token here would break quarterly. id-token: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.release-tag.outputs.tag }} # `npm ci` below runs dependency lifecycle scripts, and no step in # this job needs the git credential afterwards. persist-credentials: false - - name: Set up Node 22 - uses: actions/setup-node@v7 + # registry-url below writes an `_authToken=${NODE_AUTH_TOKEN}` line into + # .npmrc whether or not a token exists, and an npm older than 11.5.1 reads + # that empty line as "auth is configured" and never asks for an OIDC + # token, so the publish fails needing auth. 24 is the oldest Node whose + # bundled npm clears that floor (11.17.0), which is why it is pinned here + # rather than upgrading npm over the top of an older one. + - name: Set up Node 24 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22" + node-version: "24" registry-url: "https://registry.npmjs.org" cache: npm cache-dependency-path: pkg/spec/package-lock.json - # registry-url above writes an `_authToken=${NODE_AUTH_TOKEN}` line into - # .npmrc whether or not a token exists, and an npm older than 11.5.1 reads - # that empty line as "auth is configured" and never asks for an OIDC - # token, so the publish fails needing auth. Node 22 ships npm 10. - - name: Install an npm that can publish over OIDC - run: npm install -g npm@latest - - name: Install dependencies working-directory: pkg/spec run: npm ci @@ -576,20 +627,20 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.release-tag.outputs.tag }} # GoReleaser reads the tag history for its changelog. fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache: true - name: Set up JDK 17 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin java-version: "17" @@ -598,7 +649,7 @@ jobs: uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 - name: Cache Gradle - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches @@ -658,7 +709,7 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install pandoc run: sudo apt-get update && sudo apt-get install -y pandoc @@ -671,11 +722,11 @@ jobs: # which holds three ordinary files. _assets is underscore-prefixed, not # hidden, and deploy-pages serves the artifact without running Jekyll, so # it needs no .nojekyll either. - - uses: actions/upload-pages-artifact@v5 + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: build/site - - uses: actions/deploy-pages@v5 + - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 id: deployment # The one status check to point branch protection at. Without `if: always()` diff --git a/docs/development/ci.md b/docs/development/ci.md index e723a4e3..2ac40e4c 100644 --- a/docs/development/ci.md +++ b/docs/development/ci.md @@ -170,6 +170,14 @@ reinstall-and-launch cycles on iOS 26.1, 10 of them reinstalling on top of a live app, so any fix for it has to be developed on a host that can still show it failing. +The leg names a device and a runtime, `iPhone 17 Pro` on `iOS 26.2`, and boots +by the UDID that pair resolves to. Both halves matter: one runner image carries +the same phone under several runtimes, so booting by name alone is booting on +whichever one `simctl` lists first, and a seed that is only calibrated against a +runtime it did not run on says nothing. A runner image that stops carrying the +pair fails the boot step naming what it does carry, which is the cue to pick a +new pair and recalibrate rather than a `bootstatus` error to read backwards. + Only one sanderling run may drive a given simulator at a time. The driver takes an advisory lock on the target's UDID and a second run is refused with the lock path in the message, because two runs interleaving app lifecycle leave the first