1name: Mobile Fingerprint Check
2β
3# Detects whether a PR changes the native fingerprint β i.e. whether merging
4# it would leave main un-OTA-able until a new store build ships. Native-change
5# PRs get the "π± Native Change" label (applied by mobile-fingerprint-label.yml,
6# which runs from main so fork PRs get it too) so they can be held and merged as a
7# batch right before the next store submission, keeping main OTA-able for
8# everything else in between. (Once one native PR merges, every later merge
9# inherits the drifted fingerprint and loses OTA reach too β that is why the
10# signal has to fire before merge, not after.)
11#
12# The check is advisory: it always passes, the label is the signal. Both
13# fingerprints are computed in this one job (same OS, same corepack-pinned
14# pnpm), so the comparison is self-consistent; no EXPO_TOKEN needed.
15on:
16 pull_request:
17 # Run even when a rebase or base change removes every native input from the
18 # diff, so the label can be cleared without installing Expo dependencies.
19 types: [opened, synchronize, reopened, edited]
20β
21concurrency:
22 group: mobile-fingerprint-check-${{ github.event.pull_request.number }}
23 cancel-in-progress: true
24β
25jobs:
26 fingerprint:
27 name: Native fingerprint diff
28 runs-on: blacksmith-8vcpu-ubuntu-2404
29 permissions:
30 contents: read
31 env:
32 APP_VARIANT: production
33 NODE_OPTIONS: --max-old-space-size=8192
34 steps:
35 - name: Checkout
36 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
37 with:
38 # The merge ref can advance after the event is queued. Pin its commit
39 # and compare against its first parent, rather than the event's base.
40 ref: ${{ github.sha }}
41 fetch-depth: 2
42β
43 - id: changes
44 name: Detect native fingerprint inputs
45 run: |
46 base_sha=$(git rev-parse HEAD^1)
47 echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
48 paths=$(git diff --no-renames --name-only "$base_sha" HEAD -- \
49 apps/mobile/ packages/client-runtime/ packages/contracts/ packages/shared/ \
50 assets/ scripts/ patches/ package.json pnpm-lock.yaml pnpm-workspace.yaml \
51 .github/workflows/mobile-fingerprint-check.yml)
52 if [[ -n "$paths" ]]; then
53 echo "relevant=true" >> "$GITHUB_OUTPUT"
54 else
55 echo "relevant=false" >> "$GITHUB_OUTPUT"
56 echo "No native fingerprint inputs changed; clearing any stale native change label." >> "$GITHUB_STEP_SUMMARY"
57 fi
58β
59 - name: Setup Vite+
60 if: steps.changes.outputs.relevant == 'true'
61 uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1.15.0
62 with:
63 node-version-file: package.json
64 cache: true
65 run-install: |
66 args:
67 - --filter=@t3tools/mobile...
68β
69 - name: Expose pnpm
70 if: steps.changes.outputs.relevant == 'true'
71 run: |
72 pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
73 vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
74 echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
75 "$vp_pnpm_bin/pnpm" --version
76β
77 - name: Fingerprint merge result
78 if: steps.changes.outputs.relevant == 'true'
79 working-directory: apps/mobile
80 run: |
81 mkdir -p "$RUNNER_TEMP/fp/head" "$RUNNER_TEMP/fp/base"
82 for platform in ios android; do
83 npx expo-updates fingerprint:generate --platform "$platform" > "$RUNNER_TEMP/fp/head/$platform.json"
84 done
85β
86 - name: Fingerprint base
87 if: steps.changes.outputs.relevant == 'true'
88 env:
89 BASE_SHA: ${{ steps.changes.outputs.base_sha }}
90 run: |
91 git checkout --quiet "$BASE_SHA"
92 # Re-sync node_modules to the base commit's lockfile before
93 # fingerprinting β a dep-changing PR must not fingerprint the base
94 # against head's installed packages.
95 pnpm install --filter=@t3tools/mobile...
96 cd apps/mobile
97 for platform in ios android; do
98 npx expo-updates fingerprint:generate --platform "$platform" > "$RUNNER_TEMP/fp/base/$platform.json"
99 done
100β
101 - id: compare
102 name: Compare fingerprints
103 if: steps.changes.outputs.relevant == 'true'
104 run: |
105 changed=""
106 {
107 echo "## Native fingerprint diff"
108 echo
109 for platform in ios android; do
110 head_hash="$(jq -r .hash "$RUNNER_TEMP/fp/head/$platform.json")"
111 base_hash="$(jq -r .hash "$RUNNER_TEMP/fp/base/$platform.json")"
112 if [ "$head_hash" = "$base_hash" ]; then
113 echo "- β
**$platform**: unchanged (\`$head_hash\`) β OTA-compatible"
114 continue
115 fi
116 changed="$changed $platform"
117 echo "- π± **$platform**: \`$base_hash\` β \`$head_hash\` β merging requires a new native build before OTAs work again"
118 jq -r -n \
119 --slurpfile h "$RUNNER_TEMP/fp/head/$platform.json" \
120 --slurpfile b "$RUNNER_TEMP/fp/base/$platform.json" '
121 ($b[0].sources | map({ (.filePath // .id): .hash }) | add // {}) as $bm
122 | $h[0].sources[]
123 | select($bm[(.filePath // .id)] != .hash)
124 | " - \(.type): `\(.filePath // .id)`"'
125 done
126 } >> "$GITHUB_STEP_SUMMARY"
127 echo "changed_platforms=${changed# }" >> "$GITHUB_OUTPUT"
128β
129 # mobile-fingerprint-label.yml reads this from main with a write token
130 # and syncs the label; this job's token is read-only for fork PRs.
131 - name: Record changed platforms
132 env:
133 CHANGED_PLATFORMS: ${{ steps.compare.outputs.changed_platforms }}
134 run: |
135 mkdir -p "$RUNNER_TEMP/fingerprint-result"
136 printf '%s\n' "$CHANGED_PLATFORMS" > "$RUNNER_TEMP/fingerprint-result/changed-platforms"
137β
138 - name: Upload changed platforms
139 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
140 with:
141 name: fingerprint-result
142 path: ${{ runner.temp }}/fingerprint-result
143 if-no-files-found: error
144 retention-days: 1