Skip to content

Commit 7d60f6b

Browse files
committed
Merge remote-tracking branch 'origin/main' into flo/sync-upstream-2026-06-18
2 parents fa56ad4 + dd452a4 commit 7d60f6b

88 files changed

Lines changed: 34109 additions & 31169 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-submit-android.yml

Lines changed: 115 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ on:
1010
options:
1111
- testflight-android
1212
- production
13+
workflow_call:
14+
inputs:
15+
profile:
16+
type: string
17+
description: Build profile to use
18+
required: true
19+
outputs:
20+
package-version:
21+
description: Version from package.json
22+
value: ${{ jobs.build.outputs.package-version }}
23+
version-code:
24+
description: Android version code
25+
value: ${{ jobs.build.outputs.version-code }}
1326

1427
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
1528
permissions:
@@ -23,6 +36,10 @@ jobs:
2336
concurrency:
2437
group: android-build
2538
cancel-in-progress: false
39+
outputs:
40+
package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
41+
version-code: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
42+
apk-artifact-name: build-${{ steps.timestamp.outputs.time }}.apk
2643
steps:
2744
- name: Check for EXPO_TOKEN
2845
run: >
@@ -66,7 +83,8 @@ jobs:
6683
run: pnpm intl:build 2>&1 | tee i18n.log
6784

6885
- name: Check for i18n compilation errors
69-
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
86+
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
87+
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
7088

7189
# EXPO_PUBLIC_ENV is handled in eas.json
7290
- name: Env
@@ -96,93 +114,72 @@ jobs:
96114
--profile $PROFILE
97115
--local --output build.aab --non-interactive
98116
99-
- name: ✍️ Rename Testflight bundle
100-
if: ${{ inputs.profile != 'production' }}
101-
run: mv build.aab build.apk
102-
103-
- name: ⏰ Get a timestamp
104-
id: timestamp
105-
run: echo "time=$(date -u +'%m-%d-%H-%M-%S')" >> "$GITHUB_OUTPUT"
106-
107-
- name: 🚀 Upload Production Artifact
108-
id: upload-artifact-production
109-
if: ${{ inputs.profile == 'production' }}
110-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
111-
with:
112-
retention-days: 30
113-
compression-level: 6
114-
name: build-${{ steps.timestamp.outputs.time }}.aab
115-
path: build.aab
116-
117-
- name: 🚀 Upload Testflight Artifact
118-
id: upload-artifact-testflight
119-
if: ${{ inputs.profile != 'production' }}
120-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
121-
with:
122-
retention-days: 30
123-
compression-level: 6
124-
name: build-${{ steps.timestamp.outputs.time }}.apk
125-
path: build.apk
126-
127117
- name: 📚 Get version from package.json
128118
id: get-build-info
129119
run: bash scripts/setGitHubOutput.sh
130120

131-
- name: 🔔 Notify Slack of Production Build
121+
- name: 🚀 Submit to Google Play
122+
env:
123+
PROFILE: ${{ inputs.profile || 'testflight-android' }}
124+
run: pnpm eas submit -p android --profile $PROFILE --non-interactive --path build.aab
125+
126+
- name: 🔔 Notify Slack of Play Store Submission
132127
if: ${{ inputs.profile == 'production' }}
133128
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
134129
with:
135130
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
136131
webhook-type: incoming-webhook
137132
payload-templated: true
138133
payload: |
139-
{"text": "Android production build for Google Play Store submission is ready!\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
134+
{"text": "Android ${{ inputs.profile || 'testflight-android' }} build submitted to Google Play!\n```Version Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
140135
141-
- name: 🔔 Notify Slack of Testflight Build
142-
if: ${{ inputs.profile != 'production' }}
143-
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
136+
- name: 🔧 Setup bundletool
137+
uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1
144138
with:
145-
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
146-
webhook-type: incoming-webhook
147-
payload-templated: true
148-
payload: |
149-
{"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact-testflight.outputs.artifact-url }}"}
139+
bundletool-version: "1.17.2"
150140

151-
- name: 🧹 Clear Metro cache
152-
if: ${{ inputs.profile == 'production' }}
153-
# https://github.com/expo/eas-cli/issues/2959#issuecomment-2749791326
154-
run: rm -rf ${TMPDIR:-/tmp}/metro-cache
141+
- name: 🔑 Decode keystore
142+
run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode >
143+
keystore.jks
155144

156-
- name: 🏗️ Build Production APK
157-
if: ${{ inputs.profile == 'production' }}
158-
run: >
159-
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
160-
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
161-
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
162-
pnpm use-build-number-with-bump
163-
pnpm eas build -p android
164-
--profile production-apk
165-
--local --output build.apk --non-interactive
145+
- name: 📦 Build signed universal APK
146+
run: |
147+
bundletool build-apks \
148+
--bundle=build.aab \
149+
--output=universal.apks \
150+
--mode=universal \
151+
--ks=keystore.jks \
152+
--ks-pass=pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
153+
--ks-key-alias=${{ secrets.ANDROID_KEY_ALIAS }} \
154+
--key-pass=pass:${{ secrets.ANDROID_KEY_PASSWORD }}
166155
167-
- name: 🚀 Upload Production APK Artifact
168-
id: upload-artifact-production-apk
169-
if: ${{ inputs.profile == 'production' }}
156+
- name: 📋 Rename to .zip for extraction
157+
run: mv universal.apks universal.zip
158+
159+
- name: 📦 Extract universal APK
160+
run: unzip -p universal.zip universal.apk > build.apk
161+
162+
- name: ⏰ Get a timestamp
163+
id: timestamp
164+
run: echo "time=$(date -u +'%m-%d-%H-%M-%S')" >> "$GITHUB_OUTPUT"
165+
166+
- name: 🚀 Upload APK Artifact
167+
id: upload-artifact
170168
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
171169
with:
172170
retention-days: 30
173171
compression-level: 6
174172
name: build-${{ steps.timestamp.outputs.time }}.apk
175173
path: build.apk
176174

177-
- name: 🔔 Notify Slack of Production APK Build
178-
if: ${{ inputs.profile == 'production' }}
175+
- name: 🔔 Notify Slack of APK Artifact
179176
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
180177
with:
181178
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
182179
webhook-type: incoming-webhook
183180
payload-templated: true
184181
payload: |
185-
{"text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
182+
{"text": "Android ${{ inputs.profile || 'testflight-android' }} APK is ready for testing!\n```Artifact: ${{ steps.upload-artifact.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
186183
187184
- name: ⬇️ Restore Cache
188185
id: get-base-commit
@@ -197,3 +194,62 @@ jobs:
197194
env:
198195
GITHUB_SHA: ${{ github.sha }}
199196
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
197+
198+
# Releases are cut from tags named after the version (e.g. "1.124.0"), so when a production
199+
# build is dispatched against such a tag we attach the APK to the matching release. This runs
200+
# as a separate job so that `contents: write` is isolated here and the build job stays read-only.
201+
attachToRelease:
202+
name: Attach APK to GitHub Release
203+
runs-on: ubuntu-latest
204+
needs: [build]
205+
if: ${{ inputs.profile == 'production' && github.ref_type == 'tag' && github.repository == 'bluesky-social/social-app' }}
206+
permissions:
207+
contents: write
208+
steps:
209+
# We only attach to a release that already exists — never create one.
210+
- name: 🔎 Check for matching GitHub Release
211+
id: release-check
212+
env:
213+
GH_TOKEN: ${{ github.token }}
214+
TAG: ${{ github.ref_name }}
215+
run: |
216+
status=$(curl -sS -o /dev/null -w '%{http_code}' \
217+
-H "Authorization: Bearer $GH_TOKEN" \
218+
-H "Accept: application/vnd.github+json" \
219+
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}")
220+
if [ "$status" = "200" ]; then
221+
echo "Found GitHub Release for tag $TAG"
222+
echo "exists=true" >> "$GITHUB_OUTPUT"
223+
else
224+
echo "No GitHub Release found for tag $TAG (HTTP $status); skipping APK attachment."
225+
echo "exists=false" >> "$GITHUB_OUTPUT"
226+
fi
227+
228+
- name: ⬇️ Download APK artifact
229+
if: ${{ steps.release-check.outputs.exists == 'true' }}
230+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
231+
with:
232+
name: ${{ needs.build.outputs.apk-artifact-name }}
233+
234+
- name: 🏷️ Rename APK for release
235+
if: ${{ steps.release-check.outputs.exists == 'true' }}
236+
run: cp build.apk "Bluesky-${{ needs.build.outputs.package-version }}.apk"
237+
238+
- name: 📎 Attach APK to GitHub Release
239+
id: attach
240+
if: ${{ steps.release-check.outputs.exists == 'true' }}
241+
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
242+
with:
243+
tag_name: ${{ github.ref_name }}
244+
files: Bluesky-${{ needs.build.outputs.package-version }}.apk
245+
fail_on_unmatched_files: true
246+
247+
- name: 🔔 Notify Slack of Release Attachment
248+
if: ${{ steps.release-check.outputs.exists == 'true' }}
249+
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
250+
with:
251+
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
252+
webhook-type: incoming-webhook
253+
payload-templated: true
254+
payload: |
255+
{"text": "Android APK attached to GitHub Release ${{ github.ref_name }}!\n```Asset: Bluesky-${{ needs.build.outputs.package-version }}.apk\nRelease: ${{ steps.attach.outputs.url }}```"}

.github/workflows/build-submit-ios.yml

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,27 @@ on:
1010
options:
1111
- testflight
1212
- production
13+
assignTestFlightGroup:
14+
type: boolean
15+
description: Assign the build to the "QA Team" TestFlight group after submitting
16+
default: false
17+
workflow_call:
18+
inputs:
19+
profile:
20+
type: string
21+
description: Build profile to use
22+
required: true
23+
assignTestFlightGroup:
24+
type: boolean
25+
description: Assign the build to the "QA Team" TestFlight group after submitting
26+
default: false
27+
outputs:
28+
package-version:
29+
description: Version from package.json
30+
value: ${{ jobs.build.outputs.package-version }}
31+
build-number:
32+
description: iOS build number
33+
value: ${{ jobs.build.outputs.build-number }}
1334

1435
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
1536
permissions:
@@ -23,6 +44,9 @@ jobs:
2344
concurrency:
2445
group: ios-build
2546
cancel-in-progress: false
47+
outputs:
48+
package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
49+
build-number: ${{ steps.ipa-build-number.outputs.build-number }}
2650
steps:
2751
- name: Check for EXPO_TOKEN
2852
run: >
@@ -61,10 +85,16 @@ jobs:
6185
with:
6286
xcode-version: "26.4"
6387

64-
- name: ☕️ Setup Cocoapods
65-
uses: maxim-lobanov/setup-cocoapods@8e97e1e98e6ccf42564fdf5622c8feec74199377 # v1.4.0
66-
with:
67-
version: 1.16.2
88+
- name: ☕️ Assert Cocoapods version
89+
run: |
90+
EXPECTED=1.16.2
91+
ACTUAL=$(pod --version)
92+
if [ "$ACTUAL" != "$EXPECTED" ]; then
93+
echo "Expected Cocoapods $EXPECTED but runner has $ACTUAL."
94+
echo "The version ships preinstalled with the macOS runner image: https://github.com/actions/runner-images/blob/main/images/macos/macos-26-Readme.md"
95+
echo "If the runner image changed, update EXPECTED here or reinstall the pinned version."
96+
exit 1
97+
fi
6898
6999
- name: 💾 Cache Pods
70100
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -79,7 +109,8 @@ jobs:
79109
run: pnpm intl:build 2>&1 | tee i18n.log
80110

81111
- name: Check for i18n compilation errors
82-
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
112+
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
113+
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
83114

84115
# EXPO_PUBLIC_ENV is handled in eas.json
85116
- name: ✏️ Write environment variables
@@ -157,6 +188,62 @@ jobs:
157188
id: get-build-info
158189
run: bash scripts/setGitHubOutput.sh
159190

191+
# Read the build number straight from the IPA's CFBundleVersion. This is the value
192+
# baked in at build time by use-build-number-with-bump (remote counter + 1) and the
193+
# number that actually lands in App Store Connect. `eas build:version:get` reads the
194+
# remote counter, which a --local build does not advance, so it can be off by one —
195+
# using it here would make distribute_only poll for a nonexistent build.
196+
- name: 🔢 Read build number from IPA
197+
id: ipa-build-number
198+
run: |
199+
plist_dir="$(mktemp -d)"
200+
unzip -o -q "$BUILD_DIR/Bluesky.ipa" 'Payload/*.app/Info.plist' -d "$plist_dir"
201+
plist="$(find "$plist_dir" -name Info.plist -print -quit)"
202+
build_number="$(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' "$plist")"
203+
rm -rf "$plist_dir"
204+
if [ -z "$build_number" ]; then
205+
echo "ERROR: could not read CFBundleVersion from IPA"
206+
exit 1
207+
fi
208+
echo "IPA build number: $build_number"
209+
echo "build-number=$build_number" >> "$GITHUB_OUTPUT"
210+
211+
# eas submit only uploads to App Store Connect; it can't assign a build to a
212+
# TestFlight group. fastlane's distribute_only mode skips the upload and assigns the
213+
# already-submitted build to the group, polling until Apple finishes processing it.
214+
- name: 🧪 Assign build to TestFlight group
215+
if: ${{ inputs.assignTestFlightGroup }}
216+
env:
217+
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
218+
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
219+
ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}
220+
APP_VERSION: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
221+
BUILD_NUMBER: ${{ steps.ipa-build-number.outputs.build-number }}
222+
run: |
223+
# Ensure the API key material is removed even if fastlane exits non-zero
224+
# (the step runs under `bash -e`, which would otherwise abort before cleanup).
225+
trap 'rm -f asc_api_key.json' EXIT
226+
# fastlane's Token.from_json_file expects the .p8 contents inline under "key"
227+
# (PEM with embedded newlines), not a path. jq handles the newline escaping.
228+
key_content="$(echo "$ASC_KEY_P8_BASE64" | base64 --decode)"
229+
jq -n \
230+
--arg key_id "$ASC_KEY_ID" \
231+
--arg issuer_id "$ASC_ISSUER_ID" \
232+
--arg key "$key_content" \
233+
'{key_id: $key_id, issuer_id: $issuer_id, key: $key, in_house: false}' \
234+
> asc_api_key.json
235+
# app_platform is required in non-interactive mode: distribute_only otherwise
236+
# calls fetch_app_platform, which prompts for input and crashes without a TTY.
237+
fastlane run upload_to_testflight \
238+
api_key_path:"$PWD/asc_api_key.json" \
239+
distribute_only:true \
240+
app_platform:"ios" \
241+
app_identifier:"xyz.blueskyweb.app" \
242+
app_version:"$APP_VERSION" \
243+
build_number:"$BUILD_NUMBER" \
244+
groups:"QA Team" \
245+
notify_external_testers:true
246+
160247
- name: 🔔 Notify Slack of Production Build
161248
if: ${{ inputs.profile == 'production' }}
162249
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
@@ -165,7 +252,7 @@ jobs:
165252
webhook-type: incoming-webhook
166253
payload-templated: true
167254
payload: |
168-
{"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}```"}
255+
{"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.ipa-build-number.outputs.build-number }}```"}
169256
170257
- name: ⬇️ Restore Cache
171258
id: get-base-commit

0 commit comments

Comments
 (0)