Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

OTA deployments on PR comment action (#8713)

authored by

hailey and committed by
GitHub
1ee91d2c 9e65f00c

+350 -33
+177
.github/workflows/pull-request-comment.yml
··· 1 + --- 2 + name: PR Comment Trigger 3 + 4 + on: 5 + issue_comment: 6 + types: [created] 7 + 8 + jobs: 9 + handle-comment: 10 + if: github.event.issue.pull_request 11 + runs-on: ubuntu-latest 12 + outputs: 13 + should-deploy: ${{ steps.check-org.outputs.result }} 14 + 15 + steps: 16 + - name: Check if bot is mentioned 17 + id: check-mention 18 + run: | 19 + if [[ "${{ github.event.comment.body }}" == *"@github-actions"* ]] || \ 20 + [[ "${{ github.event.comment.body }}" == *"github-actions[bot]"* ]]; then 21 + bot_mentioned=true 22 + else 23 + bot_mentioned=false 24 + fi 25 + 26 + 27 + if [[ "${{ github.event.comment.body }}" == *"ota"* ]]; then 28 + has_ota=true 29 + else 30 + has_ota=false 31 + fi 32 + 33 + 34 + if [[ "$bot_mentioned" == "true" ]] && [[ "$has_ota" == "true" ]]; then 35 + echo "mentioned=true" >> $GITHUB_OUTPUT 36 + else 37 + echo "mentioned=false" >> $GITHUB_OUTPUT 38 + fi 39 + 40 + - name: Check organization membership 41 + if: steps.check-mention.outputs.mentioned == 'true' 42 + id: check-org 43 + uses: actions/github-script@v7 44 + with: 45 + script: | 46 + try { 47 + const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ 48 + owner: context.repo.owner, 49 + repo: context.repo.repo, 50 + username: context.payload.comment.user.login 51 + }); 52 + 53 + const hasAccess = ['admin', 'write'].includes(perm.permission); 54 + console.log(`User has ${perm.permission} access`); 55 + 56 + return hasAccess; 57 + } catch(error) { 58 + console.log('User has no repository access'); 59 + return false; 60 + } 61 + 62 + bundle-deploy: 63 + name: Bundle and Deploy EAS Update 64 + runs-on: ubuntu-latest 65 + needs: [handle-comment] 66 + if: needs.handle-comment.outputs.should-deploy == 'true' 67 + concurrency: 68 + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy 69 + cancel-in-progress: true 70 + 71 + steps: 72 + - name: 💬 Drop a comment 73 + uses: marocchino/sticky-pull-request-comment@v2 74 + with: 75 + header: pull-request-eas-build-${{ github.sha }} 76 + message: | 77 + An OTA deployment has been requested and is now running... 78 + 79 + [Here is some music to listen to while you wait...](https://www.youtube.com/watch?v=VBlFHuCzPgY) 80 + --- 81 + *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* 82 + 83 + - name: Check for EXPO_TOKEN 84 + run: > 85 + if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then 86 + echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" 87 + exit 1 88 + fi 89 + 90 + - name: ⬇️ Checkout 91 + uses: actions/checkout@v4 92 + 93 + - name: 🔧 Setup Node 94 + uses: actions/setup-node@v4 95 + with: 96 + node-version-file: .nvmrc 97 + cache: yarn 98 + 99 + - name: Install dependencies 100 + run: yarn install --frozen-lockfile 101 + 102 + - name: Lint check 103 + run: yarn lint 104 + 105 + - name: Lint lockfile 106 + run: yarn lockfile-lint 107 + 108 + - name: 🔤 Compile translations 109 + run: yarn intl:build 2>&1 | tee i18n.log 110 + 111 + - name: Check for i18n compilation errors 112 + 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 113 + 114 + - name: Type check 115 + run: yarn typecheck 116 + 117 + - name: 🔨 Setup EAS 118 + uses: expo/expo-github-action@v8 119 + with: 120 + expo-version: latest 121 + eas-version: latest 122 + token: ${{ secrets.EXPO_TOKEN }} 123 + 124 + - name: ⛏️ Setup Expo 125 + run: yarn global add eas-cli-local-build-plugin 126 + 127 + - name: 🪛 Setup jq 128 + uses: dcarbone/install-jq-action@v2 129 + 130 + - name: ✏️ Write environment variables 131 + run: | 132 + export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' 133 + echo "${{ secrets.ENV_TOKEN }}" > .env 134 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env 135 + echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env 136 + echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 137 + echo "$json" > google-services.json 138 + 139 + - name: Setup Sentry vars for build-time injection 140 + id: sentry 141 + run: | 142 + echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 143 + echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 144 + 145 + - name: 🏗️ Create Bundle 146 + run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="pull-request" yarn export 147 + 148 + - name: 📦 Package Bundle and 🚀 Deploy 149 + run: yarn use-build-number bash scripts/bundleUpdate.sh 150 + env: 151 + DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }} 152 + CHANNEL_NAME: pull-request-${{ github.event.issue.number }} 153 + 154 + 155 + - name: 💬 Drop a comment 156 + uses: marocchino/sticky-pull-request-comment@v2 157 + with: 158 + header: pull-request-eas-build-${{ github.sha }} 159 + message: | 160 + Your requested OTA deployment was successful! You may now apply it by pressing the link below. 161 + 162 + [Apply OTA update](bluesky://ota-apply?channel=pull-request-${{ github.event.issue.number }}) 163 + 164 + [Here is some music to listen to while you wait...](https://www.youtube.com/watch?v=VBlFHuCzPgY) 165 + --- 166 + *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* 167 + 168 + 169 + - name: 💬 Drop a comment 170 + uses: marocchino/sticky-pull-request-comment@v2 171 + if: failure() 172 + with: 173 + header: pull-request-eas-build-${{ github.sha }} 174 + message: | 175 + Your requested OTA deployment was unsuccessful. See action logs for more details. 176 + --- 177 + *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
-1
app.config.js
··· 190 190 } 191 191 : undefined, 192 192 checkAutomatically: 'NEVER', 193 - channel: UPDATES_CHANNEL, 194 193 }, 195 194 plugins: [ 196 195 'expo-video',
+20 -3
src/lib/hooks/useIntentHandler.ts
··· 1 1 import React from 'react' 2 + import {Alert} from 'react-native' 2 3 import * as Linking from 'expo-linking' 3 4 4 5 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 5 - import {logEvent} from '#/lib/statsig/statsig' 6 + import {logger} from '#/logger' 6 7 import {isNative} from '#/platform/detection' 7 8 import {useSession} from '#/state/session' 8 9 import {useCloseAllActiveElements} from '#/state/util' ··· 12 13 } from '#/components/ageAssurance/AgeAssuranceRedirectDialog' 13 14 import {useIntentDialogs} from '#/components/intents/IntentDialogs' 14 15 import {Referrer} from '../../../modules/expo-bluesky-swiss-army' 16 + import {IS_TESTFLIGHT} from '../app-info.web' 17 + import {useApplyPullRequestOTAUpdate} from './useOTAUpdates' 15 18 16 - type IntentType = 'compose' | 'verify-email' | 'age-assurance' 19 + type IntentType = 'compose' | 'verify-email' | 'age-assurance' | 'apply-ota' 17 20 18 21 const VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/ 19 22 ··· 27 30 const ageAssuranceRedirectDialogControl = 28 31 useAgeAssuranceRedirectDialogControl() 29 32 const {currentAccount} = useSession() 33 + const {tryApplyUpdate} = useApplyPullRequestOTAUpdate() 30 34 31 35 React.useEffect(() => { 32 36 const handleIncomingURL = (url: string) => { 33 37 const referrerInfo = Referrer.getReferrerInfo() 34 38 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { 35 - logEvent('deepLink:referrerReceived', { 39 + logger.metric('deepLink:referrerReceived', { 36 40 to: url, 37 41 referrer: referrerInfo?.referrer, 38 42 hostname: referrerInfo?.hostname, ··· 92 96 } 93 97 return 94 98 } 99 + case 'apply-ota': { 100 + if (!isNative || !IS_TESTFLIGHT) { 101 + return 102 + } 103 + 104 + const channel = params.get('channel') 105 + if (!channel) { 106 + Alert.alert('Error', 'No channel provided to look for.') 107 + } else { 108 + tryApplyUpdate(channel) 109 + } 110 + } 95 111 default: { 96 112 return 97 113 } ··· 111 127 verifyEmailIntent, 112 128 ageAssuranceRedirectDialogControl, 113 129 currentAccount, 130 + tryApplyUpdate, 114 131 ]) 115 132 } 116 133
+137 -29
src/lib/hooks/useOTAUpdates.ts
··· 1 1 import React from 'react' 2 - import {Alert, AppState, AppStateStatus} from 'react-native' 2 + import {Alert, AppState, type AppStateStatus} from 'react-native' 3 3 import {nativeBuildVersion} from 'expo-application' 4 4 import { 5 5 checkForUpdateAsync, ··· 29 29 ) 30 30 } 31 31 32 + async function setExtraParamsPullRequest(channel: string) { 33 + await setExtraParamAsync( 34 + isIOS ? 'ios-build-number' : 'android-build-number', 35 + // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. 36 + // This just ensures it gets passed as a string 37 + `${nativeBuildVersion}`, 38 + ) 39 + await setExtraParamAsync('channel', channel) 40 + } 41 + 42 + async function updateTestflight() { 43 + await setExtraParams() 44 + 45 + const res = await checkForUpdateAsync() 46 + if (res.isAvailable) { 47 + await fetchUpdateAsync() 48 + 49 + Alert.alert( 50 + 'Update Available', 51 + 'A new version of the app is available. Relaunch now?', 52 + [ 53 + { 54 + text: 'No', 55 + style: 'cancel', 56 + }, 57 + { 58 + text: 'Relaunch', 59 + style: 'default', 60 + onPress: async () => { 61 + await reloadAsync() 62 + }, 63 + }, 64 + ], 65 + ) 66 + } 67 + } 68 + 69 + export function useApplyPullRequestOTAUpdate() { 70 + const {currentlyRunning} = useUpdates() 71 + const [pending, setPending] = React.useState(false) 72 + const currentChannel = currentlyRunning?.channel 73 + const isCurrentlyRunningPullRequestDeployment = 74 + currentChannel?.startsWith('pull-request') 75 + 76 + const tryApplyUpdate = async (channel: string) => { 77 + setPending(true) 78 + if (currentChannel === channel) { 79 + const res = await checkForUpdateAsync() 80 + if (res.isAvailable) { 81 + logger.debug('Attempting to fetch update...') 82 + await fetchUpdateAsync() 83 + Alert.alert( 84 + 'Deployment Available', 85 + `A new deployment of ${channel} is availalble. Relaunch now?`, 86 + [ 87 + { 88 + text: 'No', 89 + style: 'cancel', 90 + }, 91 + { 92 + text: 'Relaunch', 93 + style: 'default', 94 + onPress: async () => { 95 + await reloadAsync() 96 + }, 97 + }, 98 + ], 99 + ) 100 + } else { 101 + Alert.alert( 102 + 'No Deployment Available', 103 + `No new deployments of ${channel} are currently available for your current native build.`, 104 + ) 105 + } 106 + } else { 107 + setExtraParamsPullRequest(channel) 108 + const res = await checkForUpdateAsync() 109 + if (res.isAvailable) { 110 + Alert.alert( 111 + 'Deployment Available', 112 + `A deployment of ${channel} is availalble. Applying this deployment may result in a bricked installation, in which case you will need to reinstall the app and may lose local data. Are you sure you want to proceed?`, 113 + [ 114 + { 115 + text: 'No', 116 + style: 'cancel', 117 + }, 118 + { 119 + text: 'Relaunch', 120 + style: 'default', 121 + onPress: async () => { 122 + await reloadAsync() 123 + }, 124 + }, 125 + ], 126 + ) 127 + } else { 128 + Alert.alert( 129 + 'No Deployment Available', 130 + `No new deployments of ${channel} are currently available for your current native build.`, 131 + ) 132 + } 133 + } 134 + setPending(false) 135 + } 136 + 137 + const revertToEmbedded = async () => { 138 + try { 139 + await updateTestflight() 140 + } catch (e: any) { 141 + logger.error('Internal OTA Update Error', {error: `${e}`}) 142 + } 143 + } 144 + 145 + return { 146 + tryApplyUpdate, 147 + revertToEmbedded, 148 + currentChannel, 149 + isCurrentlyRunningPullRequestDeployment, 150 + pending, 151 + } 152 + } 153 + 32 154 export function useOTAUpdates() { 33 155 const shouldReceiveUpdates = isEnabled && !__DEV__ 34 156 ··· 36 158 const lastMinimize = React.useRef(0) 37 159 const ranInitialCheck = React.useRef(false) 38 160 const timeout = React.useRef<NodeJS.Timeout>() 39 - const {isUpdatePending} = useUpdates() 161 + const {currentlyRunning, isUpdatePending} = useUpdates() 162 + const currentChannel = currentlyRunning?.channel 40 163 41 164 const setCheckTimeout = React.useCallback(() => { 42 165 timeout.current = setTimeout(async () => { ··· 60 183 61 184 const onIsTestFlight = React.useCallback(async () => { 62 185 try { 63 - await setExtraParams() 64 - 65 - const res = await checkForUpdateAsync() 66 - if (res.isAvailable) { 67 - await fetchUpdateAsync() 68 - 69 - Alert.alert( 70 - 'Update Available', 71 - 'A new version of the app is available. Relaunch now?', 72 - [ 73 - { 74 - text: 'No', 75 - style: 'cancel', 76 - }, 77 - { 78 - text: 'Relaunch', 79 - style: 'default', 80 - onPress: async () => { 81 - await reloadAsync() 82 - }, 83 - }, 84 - ], 85 - ) 86 - } 186 + await updateTestflight() 87 187 } catch (e: any) { 88 188 logger.error('Internal OTA Update Error', {error: `${e}`}) 89 189 } 90 190 }, []) 91 191 92 192 React.useEffect(() => { 193 + // We don't need to check anything if the current update is a PR update 194 + if (currentChannel?.startsWith('pull-request')) { 195 + return 196 + } 197 + 93 198 // We use this setTimeout to allow Statsig to initialize before we check for an update 94 199 // For Testflight users, we can prompt the user to update immediately whenever there's an available update. This 95 200 // is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update ··· 103 208 104 209 setCheckTimeout() 105 210 ranInitialCheck.current = true 106 - }, [onIsTestFlight, setCheckTimeout, shouldReceiveUpdates]) 211 + }, [onIsTestFlight, currentChannel, setCheckTimeout, shouldReceiveUpdates]) 107 212 108 213 // After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available 109 214 // or B check for an update again. 110 215 React.useEffect(() => { 111 - if (!isEnabled) return 216 + // We also don't start this timeout if the user is on a pull request update 217 + if (!isEnabled || currentChannel?.startsWith('pull-request')) { 218 + return 219 + } 112 220 113 221 const subscription = AppState.addEventListener( 114 222 'change', ··· 138 246 clearTimeout(timeout.current) 139 247 subscription.remove() 140 248 } 141 - }, [isUpdatePending, setCheckTimeout]) 249 + }, [isUpdatePending, currentChannel, setCheckTimeout]) 142 250 }
+16
src/screens/Settings/Settings.tsx
··· 12 12 import {IS_INTERNAL} from '#/lib/app-info' 13 13 import {HELP_DESK_URL} from '#/lib/constants' 14 14 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 15 + import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' 15 16 import { 16 17 type CommonNavigatorParams, 17 18 type NavigationProp, 18 19 } from '#/lib/routes/types' 19 20 import {sanitizeDisplayName} from '#/lib/strings/display-names' 20 21 import {sanitizeHandle} from '#/lib/strings/handles' 22 + import {isNative} from '#/platform/detection' 21 23 import {useProfileShadow} from '#/state/cache/profile-shadow' 22 24 import * as persisted from '#/state/persisted' 23 25 import {clearStorage} from '#/state/persisted' ··· 364 366 const onboardingDispatch = useOnboardingDispatch() 365 367 const navigation = useNavigation<NavigationProp>() 366 368 const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration() 369 + const { 370 + revertToEmbedded, 371 + isCurrentlyRunningPullRequestDeployment, 372 + currentChannel, 373 + } = useApplyPullRequestOTAUpdate() 367 374 const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged() 368 375 369 376 const resetOnboarding = async () => { ··· 452 459 <Trans>Clear all storage data (restart after this)</Trans> 453 460 </SettingsList.ItemText> 454 461 </SettingsList.PressableItem> 462 + {isNative && isCurrentlyRunningPullRequestDeployment ? ( 463 + <SettingsList.PressableItem 464 + onPress={revertToEmbedded} 465 + label={_(msg`Unapply Pull Request`)}> 466 + <SettingsList.ItemText> 467 + <Trans>Unapply Pull Request {currentChannel}</Trans> 468 + </SettingsList.ItemText> 469 + </SettingsList.PressableItem> 470 + ) : null} 455 471 </> 456 472 ) 457 473 }