From fbcd5422ee629d750adbe5b90041dfc046cf9c78 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:55:56 +0000 Subject: [PATCH] Build PR OTA bundle in parallel with the fingerprint check publish-pr-ota needed fingerprint-native, so the bundle build - the slow half - did not start until the fingerprint had finished, even though the bundle does not depend on the fingerprint verdict. Only the decision to publish it does. Split the job in two: build-pr-ota builds and assembles the bundle with no dependency on the fingerprint (so it runs alongside it) and hands it over as a short-lived artifact, and publish-pr-ota keeps the fingerprint gate and does just the denis publish. A dirty fingerprint now discards an already-built bundle instead of saving the build, which costs a runner but takes the fingerprint's wall clock off the critical path of every fingerprint-clean PR. Since the build job already has the EAS CLI set up, it also reads the native build numbers and exposes them (along with the release version) as job outputs, so the publish job needs neither EXPO_TOKEN nor node_modules and can invoke denisPublish.sh directly rather than through the use-build-number wrapper. denisPublish.sh gains SKIP_BUNDLE_ASSEMBLY for that case, where bundleTempDir arrives as an artifact and ./dist is not present; without it the script assembles the directory as before, so the main-branch deploy is unaffected. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UL83tcSq9MvwBkUGJGRyWV --- .github/workflows/pull-request-commit.yml | 113 +++++++++++++++++----- scripts/denisPublish.sh | 17 +++- 2 files changed, 103 insertions(+), 27 deletions(-) diff --git a/.github/workflows/pull-request-commit.yml b/.github/workflows/pull-request-commit.yml index a06556ea15..4b3b9c42e7 100644 --- a/.github/workflows/pull-request-commit.yml +++ b/.github/workflows/pull-request-commit.yml @@ -19,7 +19,7 @@ concurrency: permissions: {} # denis release tag in bluesky-social/tango whose linux-amd64 binary the PR OTA -# job downloads. Bump this one line to roll denis. +# publish job downloads. Bump this one line to roll denis. env: DENIS_RELEASE_TAG: denis-v0.1.1 @@ -255,25 +255,24 @@ jobs: # every dependabot PR. There is no OTA preview worth publishing for a # dependency bump anyway. # - # Gated on a clean fingerprint-native run: an OTA can only carry JS, so once - # the native surface changes the published bundle no longer represents the PR - # and installing it on a store/TestFlight client is misleading at best. Those - # PRs need a native build instead. A skipped or failed fingerprint job also - # skips this one - without a verdict we can't say the OTA is representative. - publish-pr-ota: - name: Publish PR OTA to denis - needs: fingerprint-native + # The bundle is built here rather than in publish-pr-ota so it runs in + # PARALLEL with fingerprint-native instead of after it: both are multi-minute + # jobs, and the bundle does not depend on the fingerprint verdict - only the + # decision to publish it does. This job deliberately carries no AWS or denis + # credentials; it only produces an artifact. When the fingerprint turns out to + # be dirty the bundle is simply discarded unpublished, which costs a runner but + # saves the fingerprint's wall clock on every clean PR. + build-pr-ota: + name: Build PR OTA bundle runs-on: ubuntu-latest if: >- github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.user.type != 'Bot' && - needs.fingerprint-native.outputs.includes-changes != 'true' + github.event.pull_request.user.type != 'Bot' concurrency: - group: pr-ota-${{ github.event.pull_request.number }} + group: pr-ota-build-${{ github.event.pull_request.number }} cancel-in-progress: true permissions: - id-token: write contents: read outputs: release-version: ${{ steps.env.outputs.release-version }} @@ -311,6 +310,71 @@ jobs: SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }} pnpm export + # Assembling here (rather than inside denisPublish.sh) keeps the handoff to + # the publish job to just the files denis uploads, instead of all of ./dist + # plus a Node toolchain to process it. + - name: 📦 Assemble bundle directory + run: node scripts/bundleUpdate.js + + # Read from the global EAS counters while the EAS CLI is still set up, so + # the publish job needs neither EXPO_TOKEN nor node_modules. These are the + # numbers the install link advertises, so the publish must be pinned to the + # same ones. + - name: 🔢 Get native build numbers + id: build-info + run: bash scripts/setGitHubOutput.sh + + # The name is keyed on the run rather than the attempt so that re-running + # only the failed jobs still finds the bundle this run already built; + # overwrite keeps a full re-run (which rebuilds it) from colliding with the + # previous attempt's upload. + - name: 🚀 Upload bundle + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-ota-bundle-${{ github.run_id }} + path: bundleTempDir + retention-days: 1 + if-no-files-found: error + overwrite: true + + # Publish the bundle built above, once fingerprint-native has returned a clean + # verdict. An OTA can only carry JS, so once the native surface changes the + # published bundle no longer represents the PR and installing it on a + # store/TestFlight client is misleading at best. Those PRs need a native build + # instead. A skipped or failed fingerprint job also skips this one - without a + # verdict we can't say the OTA is representative. + # + # This is kept separate from build-pr-ota so the fingerprint gate holds back + # only the publish, which is quick, rather than the whole bundle build. + publish-pr-ota: + name: Publish PR OTA to denis + needs: [fingerprint-native, build-pr-ota] + runs-on: ubuntu-latest + if: >- + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.type != 'Bot' && + needs.fingerprint-native.outputs.includes-changes != 'true' + concurrency: + group: pr-ota-publish-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: + id-token: write + contents: read + steps: + # Needed for scripts/denisPublish.sh and the local composite actions; no + # dependency install, since denis is a downloaded binary. + - name: ⬇️ Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: ⬇️ Download bundle + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-ota-bundle-${{ github.run_id }} + path: bundleTempDir + - name: ☁️ Configure AWS credentials (denis, PR-scoped) uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 with: @@ -346,22 +410,23 @@ jobs: app-id: ${{ vars.SYNC_INTERNAL_APP_ID }} private-key: ${{ secrets.SYNC_INTERNAL_PK }} - - name: 🔢 Get native build numbers - id: build-info - run: bash scripts/setGitHubOutput.sh - + # The use-build-number wrapper is skipped: every value it would look up is + # already fixed by the build job, and calling it would mean installing + # dependencies again just to shell out to the EAS CLI. - name: 🚀 Publish OTA to denis (S3) - run: pnpm use-build-number bash scripts/denisPublish.sh + run: bash scripts/denisPublish.sh env: - RUNTIME_VERSION: '' + # bundleTempDir arrived as an artifact; ./dist is not in this job. + SKIP_BUNDLE_ASSEMBLY: '1' CHANNEL_NAME: pull-request-${{ github.event.pull_request.number }} # Pin the publish to the same values exposed in the install link. - BSKY_IOS_BUILD_NUMBER: ${{ steps.build-info.outputs.BSKY_IOS_BUILD_NUMBER }} - BSKY_ANDROID_VERSION_CODE: ${{ steps.build-info.outputs.BSKY_ANDROID_VERSION_CODE }} + RUNTIME_VERSION: ${{ needs.build-pr-ota.outputs.release-version }} + BSKY_IOS_BUILD_NUMBER: ${{ needs.build-pr-ota.outputs.ios-build-number }} + BSKY_ANDROID_VERSION_CODE: ${{ needs.build-pr-ota.outputs.android-build-number }} comment-pr-ota: name: Comment PR OTA install link - needs: publish-pr-ota + needs: [build-pr-ota, publish-pr-ota] runs-on: ubuntu-latest permissions: pull-requests: write @@ -373,6 +438,6 @@ jobs: message: | The OTA deployment for this PR was successful! You may now apply it by either scanning the QR code or opening the deep link below in your browser: - QR code for the PR OTA deployment + QR code for the PR OTA deployment - `bluesky://intent/apply-ota?channel=pull-request-${{ github.event.pull_request.number }}&releaseVersion=${{ needs.publish-pr-ota.outputs.release-version }}&iosBuildNumber=${{ needs.publish-pr-ota.outputs.ios-build-number }}&androidBuildNumber=${{ needs.publish-pr-ota.outputs.android-build-number }}` + `bluesky://intent/apply-ota?channel=pull-request-${{ github.event.pull_request.number }}&releaseVersion=${{ needs.build-pr-ota.outputs.release-version }}&iosBuildNumber=${{ needs.build-pr-ota.outputs.ios-build-number }}&androidBuildNumber=${{ needs.build-pr-ota.outputs.android-build-number }}` diff --git a/scripts/denisPublish.sh b/scripts/denisPublish.sh index c6a30b77b9..48fd998fef 100755 --- a/scripts/denisPublish.sh +++ b/scripts/denisPublish.sh @@ -10,10 +10,21 @@ set -o nounset # (configure-aws-credentials OIDC), and BSKY_IOS_BUILD_NUMBER / # BSKY_ANDROID_VERSION_CODE from the use-build-number wrapper. -rm -rf bundleTempDir +# Set when bundleTempDir was assembled by a separate job and handed over as an +# artifact, as the PR OTA flow does so the bundle can be built in parallel with +# the fingerprint check. Without it, assemble the directory from ./dist here. +if [ -n "${SKIP_BUNDLE_ASSEMBLY:-}" ]; then + echo "Using pre-assembled bundle directory..." + if [ ! -f bundleTempDir/metadata.json ]; then + echo "bundleTempDir/metadata.json is missing; nothing to publish" >&2 + exit 1 + fi +else + rm -rf bundleTempDir -echo "Assembling bundle directory..." -node scripts/bundleUpdate.js + echo "Assembling bundle directory..." + node scripts/bundleUpdate.js +fi if [ -z "$RUNTIME_VERSION" ]; then RUNTIME_VERSION=$(cat package.json | jq '.version' -r)