# Credit for fingerprint action https://github.com/expo/expo # https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml --- name: PR Tests on: push: branches: [main] pull_request: types: [opened, synchronize] concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true # Permissions are granted per-job below; anything unlisted defaults to none. # pull-requests: write is needed by sticky-pull-request-comment to post the # bundle-size and fingerprint diffs and the PR OTA install link permissions: {} # denis release tag in bluesky-social/tango whose linux-amd64 binary the PR OTA # job downloads. Bump this one line to roll denis. env: DENIS_RELEASE_TAG: denis-v0.1.1 NODE_OPTIONS: --max-old-space-size=4096 jobs: # Populate this from main so every PR can restore the same trusted baseline. webpack-analyzer-base: runs-on: ubuntu-24.04 if: ${{ github.event_name == 'push' }} permissions: contents: read steps: - name: โฌ‡๏ธ Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: ๐Ÿ”ง Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm - name: โฌ‡๏ธ Get base stats from cache id: get-base-stats uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: stats.json key: stats-base-main-${{ github.sha }} - name: ๐Ÿ”ฆ Generate stats file for base commit if: ${{ !steps.get-base-stats.outputs.cache-hit }} run: | pnpm install pnpm intl:build pnpm generate-webpack-stats-file - name: โฌ†๏ธ Save base stats to cache if: ${{ !steps.get-base-stats.outputs.cache-hit }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: stats.json key: stats-base-main-${{ github.sha }} webpack-analyzer: runs-on: ubuntu-24.04 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}} permissions: contents: read pull-requests: write steps: - name: โฌ‡๏ธ Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: ๐Ÿ”ง Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm - name: ๐ŸŒฟ Ensure tracking relevant branches and checkout base env: HEAD_REF: ${{ github.head_ref }} BASE_REF: ${{ github.base_ref }} run: | git checkout $HEAD_REF git checkout $BASE_REF - name: ๐Ÿ” Get the base commit id: base-commit env: BASE_REF: ${{ github.base_ref }} run: echo base-commit=$(git log -n 1 $BASE_REF --pretty=format:'%H') >> "$GITHUB_OUTPUT" - name: ๐Ÿ”€ Merge PR commit env: HEAD_REF: ${{ github.head_ref }} run: | # Have to set a git config for the merge to work git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" git merge --no-edit $HEAD_REF pnpm install pnpm intl:build - name: ๐Ÿ”ฆ Generate stats file for PR run: | pnpm generate-webpack-stats-file mv stats.json ../stats-new.json - name: โฌ‡๏ธ Get base stats from cache id: get-base-stats # Restore-only prevents PR-scoped fallback builds from creating caches. uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: stats.json key: stats-base-main-${{ steps.base-commit.outputs.base-commit }} - name: โช Restore to base commit if: ${{ !steps.get-base-stats.outputs.cache-hit }} env: BASE_COMMIT: ${{ steps.base-commit.outputs.base-commit }} run: | git reset "$BASE_COMMIT" git restore . - name: ๐Ÿ”ฆ Generate stats file from base commit if: ${{ !steps.get-base-stats.outputs.cache-hit }} run: | pnpm install pnpm intl:build pnpm generate-webpack-stats-file - name: ๏ผ… Get diff id: get-diff uses: NejcZdovc/bundle-size-diff@5321de41d2d62a7b0f4d6e60f59d1280a0034160 # v1.1.0 with: base_path: "stats.json" pr_path: "../stats-new.json" excluded_assets: "(.+).chunk.js|(.+).js.map|(.+).json|(.+).png|(.+).svg|(.+).webp|(.+).jpg|(.+).ico" - name: ๐Ÿ’ฌ Drop a comment uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: bundle-diff message: | | Old size | New size | Diff | |----------|----------|-----------------------| | ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) | --- fingerprint-native: runs-on: ubuntu-22.04 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}} permissions: contents: read pull-requests: write outputs: # Empty when the native surface is unchanged, 'true' when it changed. # publish-pr-ota gates on this. includes-changes: ${{ steps.fingerprint.outputs.includes-changes }} steps: - name: โฌ‡๏ธ Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 100 - name: โฌ‡๏ธ Fetch commits from base branch run: git fetch origin main:main --depth 100 if: github.event_name == 'pull_request' - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: ๐Ÿ”ง Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm - name: ๐Ÿ“ท Check fingerprint and install dependencies id: fingerprint timeout-minutes: 5 uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0 with: profile: pull-request - name: ๐Ÿ’ฌ Drop a comment uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 if: ${{ steps.fingerprint.outputs.includes-changes }} with: header: fingerprint-diff message: | The Pull Request introduced native fingerprint changes against the base commit. --- *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) ๐Ÿค–* - name: ๐Ÿ’ฌ Delete comment uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 if: ${{ !steps.fingerprint.outputs.includes-changes }} with: header: fingerprint-diff delete: true # publish-pr-ota is skipped once the fingerprint changes, so any install # link left over from an earlier fingerprint-clean commit on this PR now # points at a bundle that no longer matches the PR. Drop it. - name: ๐Ÿ’ฌ Delete stale OTA install comment uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 if: ${{ steps.fingerprint.outputs.includes-changes }} with: header: pull-request-ota delete: true - name: ๐Ÿท๏ธ Label as fingerprint changed if: ${{ steps.fingerprint.outputs.includes-changes }} env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | gh pr edit "$PR_NUMBER" --add-label "bot: fingerprint changed" || true - name: ๐Ÿท๏ธ Remove fingerprint changed label if: ${{ !steps.fingerprint.outputs.includes-changes }} env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | gh pr edit "$PR_NUMBER" --remove-label "bot: fingerprint changed" || true # Automatic per-PR OTA preview, published to the pull-request- channel on # denis. Replaces the old `@github-actions ota` comment trigger. Gated to # same-repo PRs (fork guard): a branch can only exist in this repo if someone # with write access pushed it, so an outside contributor (who can only open a # PR from a fork) never runs this job with the denis publish role in scope. # This matches the fork-guard gate the other jobs in this workflow use; # author_association is deliberately NOT checked (it can't identify a private # org member and would skip their PRs). # # Bot authors are excluded: Dependabot pushes in-repo branches, so it passes # the fork guard, but GitHub withholds repo secrets from Dependabot-triggered # runs. EXPO_TOKEN is then empty and the job fails at setup โ€” a red check on # 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 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' && vars.OTA_FINGERPRINT_PIPELINE_ENABLED != 'true' && needs.fingerprint-native.outputs.includes-changes != 'true' concurrency: group: pr-ota-${{ github.event.pull_request.number }} cancel-in-progress: true permissions: id-token: write contents: read outputs: release-version: ${{ steps.env.outputs.release-version }} ios-build-number: ${{ steps.build-info.outputs.BSKY_IOS_BUILD_NUMBER }} android-build-number: ${{ steps.build-info.outputs.BSKY_ANDROID_VERSION_CODE }} steps: - name: โฌ‡๏ธ Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.head.sha }} - name: ๐Ÿ› ๏ธ Setup Expo project uses: ./.github/actions/setup-expo-project with: expo-token: ${{ secrets.EXPO_TOKEN }} - name: ๐Ÿ”ค Compile translations uses: ./.github/actions/compile-i18n - name: โœ๏ธ Write environment variables id: env uses: ./.github/actions/write-env with: env-token: ${{ secrets.ENV_TOKEN }} sentry-dsn: ${{ secrets.SENTRY_DSN }} bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }} gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} expo-public-env: testflight - name: ๐Ÿ—๏ธ Create Bundle run: > SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_RELEASE=${{ steps.env.outputs.release-version }} SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }} pnpm export - name: โ˜๏ธ Configure AWS credentials (denis, PR-scoped) uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: arn:aws:iam::007404326489:role/denis-ci-publish-pr aws-region: us-east-2 # Defense-in-depth: the base role is already scoped to pr/*, but narrow # this session further to just THIS PR's prefix so a bug can't write to # another PR's objects or the prod tree. inline-session-policy: |- { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:PutObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::bsky-denis-ota-prod/pr/${{ github.event.pull_request.number }}/*" }, { "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::bsky-denis-ota-prod", "Condition": { "StringLike": { "s3:prefix": "pr/${{ github.event.pull_request.number }}/*" } } } ] } - name: โฌ‡๏ธ Setup denis CLI uses: ./.github/actions/setup-denis with: release-tag: ${{ env.DENIS_RELEASE_TAG }} 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 - name: ๐Ÿš€ Publish OTA to denis (S3) run: pnpm use-build-number bash scripts/denisPublish.sh env: RUNTIME_VERSION: '' 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 }} comment-pr-ota: name: Comment PR OTA install link needs: publish-pr-ota runs-on: ubuntu-latest permissions: pull-requests: write steps: - name: ๐Ÿ’ฌ Drop OTA install comment uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: pull-request-ota 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 `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 }}` publish-pr-ota-fingerprint: name: Publish fingerprint PR OTA to denis needs: fingerprint-native 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' && vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true' concurrency: group: pr-ota-${{ github.event.pull_request.number }} cancel-in-progress: true permissions: id-token: write contents: read env: OTA_FINGERPRINT_PIPELINE_ENABLED: '1' steps: - name: ๐Ÿงญ Validate fingerprint rollout configuration env: DENIS_VERSION: ${{ vars.OTA_FINGERPRINT_DENIS_VERSION }} run: | if [ -z "$DENIS_VERSION" ]; then echo "::error::OTA_FINGERPRINT_DENIS_VERSION must pin a structured-publisher release" exit 1 fi - name: โฑ๏ธ Allocate publication version id: publication run: echo "bundle-version=$(node -e 'process.stdout.write(String(Date.now()))')" >> "$GITHUB_OUTPUT" - name: โฌ‡๏ธ Checkout exact PR head uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.head.sha }} - name: ๐Ÿ› ๏ธ Setup Expo project uses: ./.github/actions/setup-expo-project with: expo-token: ${{ secrets.EXPO_TOKEN }} - name: ๐Ÿ”ค Compile translations uses: ./.github/actions/compile-i18n - name: โœ๏ธ Write environment variables id: env uses: ./.github/actions/write-env with: env-token: ${{ secrets.ENV_TOKEN }} sentry-dsn: ${{ secrets.SENTRY_DSN }} bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }} gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} expo-public-env: testflight - name: ๐Ÿงฌ Resolve iOS runtime id: ios-runtime uses: bluesky-social/github-actions/fingerprint-runtime@b890bb3f200c5fb9fee7a2a1647e08537a73bde0 env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} with: platform: ios profile: testflight source-commit: ${{ github.event.pull_request.head.sha }} - name: ๐Ÿงฌ Resolve Android runtime id: android-runtime uses: bluesky-social/github-actions/fingerprint-runtime@b890bb3f200c5fb9fee7a2a1647e08537a73bde0 env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} with: platform: android profile: testflight source-commit: ${{ github.event.pull_request.head.sha }} - name: ๐Ÿ“‹ Stage fingerprint reports env: IOS_REPORT: ${{ steps.ios-runtime.outputs.report-path }} ANDROID_REPORT: ${{ steps.android-runtime.outputs.report-path }} run: | mkdir ota-release cp "$IOS_REPORT" ota-release/ios-fingerprint.json cp "$ANDROID_REPORT" ota-release/android-fingerprint.json - name: ๐Ÿ—๏ธ Export exact PR head per native platform env: EXPO_PUBLIC_ENV: testflight SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_RELEASE: ${{ steps.env.outputs.release-version }} SENTRY_DIST: ${{ steps.env.outputs.bundle-identifier }} run: | EAS_BUILD_PLATFORM=ios pnpm exec expo export --platform ios --output-dir ota-release/dist-ios --dump-sourcemap EAS_BUILD_PLATFORM=android pnpm exec expo export --platform android --output-dir ota-release/dist-android --dump-sourcemap - name: ๐Ÿงฌ Re-resolve runtimes after export env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} run: | node scripts/ota/resolve-runtime.mjs --platform ios --profile testflight --source-commit "${{ github.event.pull_request.head.sha }}" --output ota-release/ios-post-export.json node scripts/ota/resolve-runtime.mjs --platform android --profile testflight --source-commit "${{ github.event.pull_request.head.sha }}" --output ota-release/android-post-export.json test "$(jq -r .runtimeVersion ota-release/ios-fingerprint.json)" = "$(jq -r .runtimeVersion ota-release/ios-post-export.json)" test "$(jq -r .runtimeVersion ota-release/android-fingerprint.json)" = "$(jq -r .runtimeVersion ota-release/android-post-export.json)" - name: ๐Ÿงพ Create structured release env: SOURCE_COMMIT: ${{ github.event.pull_request.head.sha }} CHANNEL: pull-request-${{ github.event.pull_request.number }} BUNDLE_VERSION: ${{ steps.publication.outputs.bundle-version }} run: | jq -n \ --arg sourceCommit "$SOURCE_COMMIT" \ --arg channel "$CHANNEL" \ --arg bundleVersion "$BUNDLE_VERSION" \ --arg iosRuntime "$(jq -r .runtimeVersion ota-release/ios-fingerprint.json)" \ --arg androidRuntime "$(jq -r .runtimeVersion ota-release/android-fingerprint.json)" \ '{schemaVersion: 1, sourceCommit: $sourceCommit, channel: $channel, nativeProfile: "testflight", bundleVersion: $bundleVersion, platforms: { ios: {runtimeVersion: $iosRuntime, fingerprintReportRef: "ios-fingerprint.json", bundleDirectory: "dist-ios"}, android: {runtimeVersion: $androidRuntime, fingerprintReportRef: "android-fingerprint.json", bundleDirectory: "dist-android"} }}' > ota-release/ota-export.json node scripts/ota/validate-release.mjs --release-file ota-release/ota-export.json > ota-release/verification.json jq -e '.valid == true' ota-release/verification.json >/dev/null - name: โฌ†๏ธ Upload PR export evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: fingerprint-pr-export-${{ github.event.pull_request.number }}-${{ github.run_id }}-${{ github.run_attempt }} path: | ota-release/ota-export.json ota-release/ios-fingerprint.json ota-release/android-fingerprint.json retention-days: 30 if-no-files-found: error - name: โ˜๏ธ Configure AWS credentials (denis, PR-scoped) uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: arn:aws:iam::007404326489:role/denis-ci-publish-pr aws-region: us-east-2 inline-session-policy: |- { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::bsky-denis-ota-prod/pr/${{ github.event.pull_request.number }}/*" }, { "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::bsky-denis-ota-prod", "Condition": {"StringLike": {"s3:prefix": "pr/${{ github.event.pull_request.number }}/*"}} } ] } - name: โฌ‡๏ธ Setup structured denis CLI uses: ./.github/actions/setup-denis with: release-tag: ${{ vars.OTA_FINGERPRINT_DENIS_VERSION }} app-id: ${{ vars.SYNC_INTERNAL_APP_ID }} private-key: ${{ secrets.SYNC_INTERNAL_PK }} - name: ๐Ÿš€ Publish structured OTA to denis run: bash scripts/denisPublish.sh ota-release/ota-export.json env: DENIS_PUBLISH_MODE: structured comment-pr-ota-fingerprint: name: Comment fingerprint PR OTA install link needs: publish-pr-ota-fingerprint runs-on: ubuntu-latest permissions: pull-requests: write steps: - name: ๐Ÿ’ฌ Drop OTA install comment uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: pull-request-ota message: | The fingerprint OTA deployment for this PR was published. Expo will only offer it to a native build with the same platform runtime. `bluesky://intent/apply-ota?channel=pull-request-${{ github.event.pull_request.number }}&sourceCommit=${{ github.event.pull_request.head.sha }}`