stage fingerprint ota workflows and native receipts

This commit is contained in:
Samuel Newman
2026-09-05 18:56:37 +03:00
parent 50752603ce
commit 212f700937
11 changed files with 1163 additions and 5 deletions
+42 -3
View File
@@ -76,7 +76,7 @@ jobs:
cancel-in-progress: false
outputs:
package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
version-code: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
version-code: ${{ steps.android-build-number.outputs.build-number || steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -109,6 +109,8 @@ jobs:
- name: 🏗️ EAS Build
uses: ./.github/actions/eas-local-build
env:
OTA_FINGERPRINT_PIPELINE_ENABLED: ${{ vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true' && '1' || '' }}
with:
platform: android
profile: ${{ inputs.profile || 'testflight-android' }}
@@ -122,6 +124,43 @@ jobs:
id: get-build-info
run: bash scripts/setGitHubOutput.sh
- name: 🔧 Setup bundletool
if: ${{ vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true' }}
uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1
with:
version: "1.18.3"
- name: 🔢 Read build number from AAB
if: ${{ vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true' }}
id: android-build-number
run: |
build_number=$(bundletool dump manifest --bundle=build.aab --module=base --xpath=/manifest/@android:versionCode)
[[ "$build_number" =~ ^[0-9]+$ ]] || { echo "::error::Could not read numeric versionCode from AAB"; exit 1; }
echo "Android build number: $build_number"
echo "build-number=$build_number" >> "$GITHUB_OUTPUT"
- name: 🧾 Create fingerprint native-build receipt
if: ${{ vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true' }}
uses: ./.github/actions/native-build-receipt
env:
OTA_FINGERPRINT_PIPELINE_ENABLED: '1'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
platform: android
profile: ${{ (inputs.profile || 'testflight-android') == 'production' && 'production' || 'testflight' }}
artifact-path: build.aab
native-build-number: ${{ steps.android-build-number.outputs.build-number }}
default-channel: ${{ (inputs.profile || 'testflight-android') == 'production' && 'production' || 'testflight' }}
- name: ⬆️ Upload fingerprint native-build receipt
if: ${{ vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-build-android-${{ (inputs.profile || 'testflight-android') == 'production' && 'production' || 'testflight' }}-${{ steps.android-build-number.outputs.build-number }}-${{ github.run_id }}-${{ github.run_attempt }}
path: native-build-receipt
retention-days: 90
if-no-files-found: error
# Hands the built bundle off to the submit / universalApk jobs. Retention is
# deliberately short (1 day) since it's only an intra-run handoff artifact.
- name: 🚀 Upload AAB artifact
@@ -134,12 +173,12 @@ jobs:
- name: 📝 Write build summary
env:
REMOTE_VERSION_CODE: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
PACKAGED_VERSION_CODE: ${{ steps.android-build-number.outputs.build-number || steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
run: |
{
echo "### Android build number"
echo
echo "\`$REMOTE_VERSION_CODE\`"
echo "\`$PACKAGED_VERSION_CODE\`"
} >> "$GITHUB_STEP_SUMMARY"
submit:
+24
View File
@@ -135,6 +135,8 @@ jobs:
- name: 🏗️ EAS Build
uses: ./.github/actions/eas-local-build
env:
OTA_FINGERPRINT_PIPELINE_ENABLED: ${{ vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true' && '1' || '' }}
with:
platform: ios
profile: ${{ inputs.profile || 'testflight' }}
@@ -204,6 +206,28 @@ jobs:
echo "IPA build number: $build_number"
echo "build-number=$build_number" >> "$GITHUB_OUTPUT"
- name: 🧾 Create fingerprint native-build receipt
if: ${{ vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true' }}
uses: ./.github/actions/native-build-receipt
env:
OTA_FINGERPRINT_PIPELINE_ENABLED: '1'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
platform: ios
profile: ${{ inputs.profile || 'testflight' }}
artifact-path: ${{ env.BUILD_DIR }}/Bluesky.ipa
native-build-number: ${{ steps.ipa-build-number.outputs.build-number }}
default-channel: ${{ (inputs.profile || 'testflight') == 'production' && 'production' || 'testflight' }}
- name: ⬆️ Upload fingerprint native-build receipt
if: ${{ vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-build-ios-${{ inputs.profile || 'testflight' }}-${{ steps.ipa-build-number.outputs.build-number }}-${{ github.run_id }}-${{ github.run_attempt }}
path: native-build-receipt
retention-days: 90
if-no-files-found: error
# Hand the IPA and dSYM off to the submit job. Retention is deliberately short since
# this artifact only exists to bridge the two jobs within a single run.
- name: 🚀 Upload build artifact
@@ -37,7 +37,10 @@ env:
jobs:
bundleDeploy:
if: github.repository == 'bluesky-social/social-app'
if: >-
github.repository == 'bluesky-social/social-app' &&
(vars.OTA_FINGERPRINT_PIPELINE_ENABLED != 'true' ||
(github.event_name == 'workflow_dispatch' && inputs.channel == 'production'))
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
# id-token: write lets this job mint an OIDC token to assume the denis
@@ -0,0 +1,305 @@
---
name: Bundle and Deploy Fingerprint OTA
on:
push:
branches: [main]
workflow_dispatch:
inputs:
channel:
type: choice
options: [testflight, production]
default: testflight
iosBuildNumber:
type: string
description: Required exact production iOS build target
androidVersionCode:
type: string
description: Required exact production Android build target
iosReceiptRunId:
type: string
description: Optional trusted iOS native-build workflow run containing a matching receipt
iosReceiptRunAttempt:
type: string
default: '1'
description: Attempt number for the optional iOS receipt run
androidReceiptRunId:
type: string
description: Optional trusted Android native-build workflow run containing a matching receipt
androidReceiptRunAttempt:
type: string
default: '1'
description: Attempt number for the optional Android receipt run
permissions: {}
jobs:
publish:
name: Export and publish fingerprint OTA
if: >-
github.repository == 'bluesky-social/social-app' &&
vars.OTA_FINGERPRINT_PIPELINE_ENABLED == 'true'
runs-on: ubuntu-latest
concurrency:
group: fingerprint-ota-${{ github.ref }}-${{ inputs.channel || 'testflight' }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
actions: read
env:
OTA_FINGERPRINT_PIPELINE_ENABLED: '1'
CHANNEL: ${{ inputs.channel || 'testflight' }}
steps:
- name: 🧭 Validate rollout configuration
env:
DENIS_VERSION: ${{ vars.OTA_FINGERPRINT_DENIS_VERSION }}
IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
ANDROID_BUILD_NUMBER: ${{ inputs.androidVersionCode }}
run: |
if [ -z "$DENIS_VERSION" ]; then
echo "::error::OTA_FINGERPRINT_DENIS_VERSION must pin a structured-publisher release"
exit 1
fi
if [ "$CHANNEL" = production ]; then
[[ "$IOS_BUILD_NUMBER" =~ ^[0-9]+$ ]] || { echo "::error::A numeric iOS production target is required"; exit 1; }
[[ "$ANDROID_BUILD_NUMBER" =~ ^[0-9]+$ ]] || { echo "::error::A numeric Android production target is required"; 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 source commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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: ${{ inputs.channel || '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: ${{ inputs.channel || 'testflight' }}
source-commit: ${{ github.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: ${{ inputs.channel || 'testflight' }}
source-commit: ${{ github.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 per native platform
env:
EXPO_PUBLIC_ENV: ${{ inputs.channel || '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 iOS runtime after export
id: ios-runtime-after-export
uses: bluesky-social/github-actions/fingerprint-runtime@b890bb3f200c5fb9fee7a2a1647e08537a73bde0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
platform: ios
profile: ${{ inputs.channel || 'testflight' }}
source-commit: ${{ github.sha }}
- name: 🧬 Re-resolve Android runtime after export
id: android-runtime-after-export
uses: bluesky-social/github-actions/fingerprint-runtime@b890bb3f200c5fb9fee7a2a1647e08537a73bde0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
platform: android
profile: ${{ inputs.channel || 'testflight' }}
source-commit: ${{ github.sha }}
- name: 🧐 Verify export did not change native inputs
env:
IOS_POST_REPORT: ${{ steps.ios-runtime-after-export.outputs.report-path }}
ANDROID_POST_REPORT: ${{ steps.android-runtime-after-export.outputs.report-path }}
run: |
test "$(jq -r .runtimeVersion ota-release/ios-fingerprint.json)" = "$(jq -r .runtimeVersion "$IOS_POST_REPORT")"
test "$(jq -r .runtimeVersion ota-release/android-fingerprint.json)" = "$(jq -r .runtimeVersion "$ANDROID_POST_REPORT")"
- name: 🔎 Find trusted iOS native receipt
id: find-ios-receipt
if: ${{ inputs.iosReceiptRunId }}
env:
GH_TOKEN: ${{ github.token }}
run: >-
node .github/scripts/find-native-receipt.mjs
--platform ios
--build-number "${{ inputs.iosBuildNumber }}"
--run-id "${{ inputs.iosReceiptRunId }}"
--run-attempt "${{ inputs.iosReceiptRunAttempt }}"
- name: 🔎 Find trusted Android native receipt
id: find-android-receipt
if: ${{ inputs.androidReceiptRunId && always() }}
env:
GH_TOKEN: ${{ github.token }}
run: >-
node .github/scripts/find-native-receipt.mjs
--platform android
--build-number "${{ inputs.androidVersionCode }}"
--run-id "${{ inputs.androidReceiptRunId }}"
--run-attempt "${{ inputs.androidReceiptRunAttempt }}"
- name: ⬇️ Download iOS native receipt
id: download-ios-receipt
if: ${{ steps.find-ios-receipt.outputs.available == 'true' && always() }}
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ steps.find-ios-receipt.outputs.artifact-name }}
path: ota-release/ios-native-receipt
run-id: ${{ inputs.iosReceiptRunId }}
github-token: ${{ github.token }}
- name: ⬇️ Download Android native receipt
id: download-android-receipt
if: ${{ steps.find-android-receipt.outputs.available == 'true' && always() }}
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ steps.find-android-receipt.outputs.artifact-name }}
path: ota-release/android-native-receipt
run-id: ${{ inputs.androidReceiptRunId }}
github-token: ${{ github.token }}
- name: 🧐 Bind downloaded iOS receipt to its build run
id: bind-ios-receipt
if: ${{ inputs.iosReceiptRunId && always() }}
env:
EXPECTED_SOURCE_COMMIT: ${{ steps.find-ios-receipt.outputs.head-sha }}
run: |
if [ ! -f ota-release/ios-native-receipt/receipt.json ]; then
echo "::warning::iOS native receipt was unavailable; target could not be verified"
echo "available=false" >> "$GITHUB_OUTPUT"
else
test -n "$EXPECTED_SOURCE_COMMIT"
test "$(jq -r .sourceCommit ota-release/ios-native-receipt/receipt.json)" = "$EXPECTED_SOURCE_COMMIT"
echo "available=true" >> "$GITHUB_OUTPUT"
fi
- name: 🧐 Bind downloaded Android receipt to its build run
id: bind-android-receipt
if: ${{ inputs.androidReceiptRunId && always() }}
env:
EXPECTED_SOURCE_COMMIT: ${{ steps.find-android-receipt.outputs.head-sha }}
run: |
if [ ! -f ota-release/android-native-receipt/receipt.json ]; then
echo "::warning::Android native receipt was unavailable; target could not be verified"
echo "available=false" >> "$GITHUB_OUTPUT"
else
test -n "$EXPECTED_SOURCE_COMMIT"
test "$(jq -r .sourceCommit ota-release/android-native-receipt/receipt.json)" = "$EXPECTED_SOURCE_COMMIT"
echo "available=true" >> "$GITHUB_OUTPUT"
fi
- name: 🧾 Create structured release
env:
BUNDLE_VERSION: ${{ steps.publication.outputs.bundle-version }}
IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
ANDROID_BUILD_NUMBER: ${{ inputs.androidVersionCode }}
IOS_RECEIPT_AVAILABLE: ${{ steps.bind-ios-receipt.outputs.available == 'true' && 'true' || 'false' }}
ANDROID_RECEIPT_AVAILABLE: ${{ steps.bind-android-receipt.outputs.available == 'true' && 'true' || 'false' }}
run: |
jq -n \
--arg sourceCommit "$GITHUB_SHA" --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)" \
--arg iosBuild "$IOS_BUILD_NUMBER" --arg androidBuild "$ANDROID_BUILD_NUMBER" \
'{schemaVersion: 1, sourceCommit: $sourceCommit, channel: $channel,
nativeProfile: $channel, bundleVersion: $bundleVersion,
platforms: {
ios: ({runtimeVersion: $iosRuntime, fingerprintReportRef: "ios-fingerprint.json", bundleDirectory: "dist-ios"} + if $channel == "production" then ({targetNativeBuildNumber: $iosBuild} + if $ENV.IOS_RECEIPT_AVAILABLE == "true" then {targetNativeBuildReceiptRef: "ios-native-receipt/receipt.json"} else {} end) else {} end),
android: ({runtimeVersion: $androidRuntime, fingerprintReportRef: "android-fingerprint.json", bundleDirectory: "dist-android"} + if $channel == "production" then ({targetNativeBuildNumber: $androidBuild} + if $ENV.ANDROID_RECEIPT_AVAILABLE == "true" then {targetNativeBuildReceiptRef: "android-native-receipt/receipt.json"} else {} end) else {} end)
}}' > 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 export evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: fingerprint-ota-export-${{ github.run_id }}-${{ github.run_attempt }}
path: |
ota-release/ota-export.json
ota-release/ios-fingerprint.json
ota-release/android-fingerprint.json
ota-release/verification.json
retention-days: 90
if-no-files-found: error
- name: ☁️ Configure AWS credentials (denis)
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: arn:aws:iam::007404326489:role/denis-ci-publish
aws-region: us-east-2
- 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
- name: 📝 Summarize publication
run: |
{
echo "### Fingerprint OTA"
echo
echo "- Channel: \`$CHANNEL\`"
echo "- Source: \`$GITHUB_SHA\`"
echo "- Bundle version: \`${{ steps.publication.outputs.bundle-version }}\`"
if [ "$CHANNEL" = production ]; then
ios_status=$(jq -r '.platforms.ios.receiptVerification.status' ota-release/verification.json)
android_status=$(jq -r '.platforms.android.receiptVerification.status' ota-release/verification.json)
echo "- iOS native target verification: \`$ios_status\`"
echo "- Android native target verification: \`$android_status\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
+187
View File
@@ -262,6 +262,7 @@ jobs:
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 }}
@@ -370,3 +371,189 @@ jobs:
<img src="https://bsky-qr.vercel.app?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 }}" width="300" height="300" alt="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 }}`