# 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. bundle-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 bundle size from cache id: get-base-size uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: base-bundle-size.txt key: base-bundle-size-main-${{ github.sha }} - name: 🔦 Build and measure base bundle if: ${{ !steps.get-base-size.outputs.cache-hit }} run: | pnpm install pnpm intl:build pnpm build-web node scripts/measure-web-bundle.js > base-bundle-size.txt - name: ⬆️ Save base bundle size to cache if: ${{ !steps.get-base-size.outputs.cache-hit }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: base-bundle-size.txt key: base-bundle-size-main-${{ github.sha }} bundle-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: 🔦 Build and measure PR bundle run: | pnpm build-web node scripts/measure-web-bundle.js > ../pr-bundle-size.txt - name: ⬇️ Get base bundle size from cache id: get-base-size # Restore-only prevents PR-scoped fallback builds from creating caches. uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: base-bundle-size.txt key: base-bundle-size-main-${{ steps.base-commit.outputs.base-commit }} - name: ⏪ Restore to base commit if: ${{ !steps.get-base-size.outputs.cache-hit }} env: BASE_COMMIT: ${{ steps.base-commit.outputs.base-commit }} run: | git reset "$BASE_COMMIT" git restore . # Drop the PR-side export so the measure script's output-dir # auto-detection can't pick it up: a webpack base exports to # web-build/ while the PR's Metro build left dist/ behind. rm -rf dist web-build - name: 🔦 Build and measure base bundle if: ${{ !steps.get-base-size.outputs.cache-hit }} run: | pnpm install pnpm intl:build pnpm build-web node scripts/measure-web-bundle.js --allow-missing > base-bundle-size.txt - name: % Get diff id: get-diff run: | node -e ' const fs = require("node:fs") const base = Number(fs.readFileSync("base-bundle-size.txt", "utf8").trim()) const pr = Number(fs.readFileSync("../pr-bundle-size.txt", "utf8").trim()) if (!Number.isFinite(base) || !Number.isFinite(pr) || base <= 0) { console.error(`Bad measurements: base=${base} pr=${pr}`) process.exit(1) } const fmt = bytes => { const abs = Math.abs(bytes) if (abs >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} MB` if (abs >= 1024) return `${(bytes / 1024).toFixed(2)} KB` return `${bytes} B` } const diff = pr - base const out = [ `base_file_string=${fmt(base)}`, `pr_file_string=${fmt(pr)}`, `diff_file_string=${diff > 0 ? "+" : ""}${fmt(diff)}`, `percent=${((diff / base) * 100).toFixed(2)}`, ].join("\n") console.log(out) fs.appendFileSync(process.env.GITHUB_OUTPUT, out + "\n") ' - 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' && 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 }}`