PR try on (#665)

* feat:- PR changes

* feat:- PR changes

* feat:- OTA

* feat:- OTA

* fix: tests
This commit is contained in:
Ritesh Shukla
2025-11-11 22:00:49 +05:30
committed by GitHub
parent f860867d07
commit b8437898f5
8 changed files with 259 additions and 29 deletions

View File

@@ -0,0 +1,32 @@
name: Publish Over-the-Air Update PR
on:
pull_request:
paths:
- 'src/**'
jobs:
publish-ota-update:
runs-on: macos-15
steps:
- name: 🛒 Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.SIGNING_REPO_PAT }}
- name: 🖥 Setup Node 20
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🧵 Run yarn
run: yarn install --network-concurrency 1
- name: 👩‍💻 Configure Git
run: |
git config --global user.email "violet@cosmonautical.cloud"
git config --global user.name "anultravioletaurora"
- name: 🤖 Publish Android Update
run: yarn sendOTA:PR ${{ github.event.pull_request.number }}
env:
SIGNING_REPO_PAT: ${{ secrets.SIGNING_REPO_PAT }}

View File

@@ -8,6 +8,8 @@ jest.mock('react-native-nitro-ota', () => ({
checkForUpdates: jest.fn().mockResolvedValue(null),
downloadUpdate: jest.fn().mockResolvedValue(undefined),
})),
reloadApp: jest.fn(),
getStoredOtaVersion: jest.fn(() => null),
}))
// Update the existing nitro-modules mock to include createHybridObject

View File

@@ -32,6 +32,7 @@
"createBundle:ios": "mkdir -p ios/App-Bundles && react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/App-Bundles/main.jsbundle --assets-dest ios/App-Bundles",
"sendOTA:android": "bash scripts/ota-android.sh",
"sendOTA:iOS": "bash scripts/ota-iOS.sh",
"sendOTA:PR": "bash scripts/ota-PR.sh",
"android-build": "cd android && ./gradlew generateCodegenArtifactsFromSchema && ./gradlew assembleRelease",
"postinstall": "patch-package"
},

53
scripts/ota-PR.sh Normal file
View File

@@ -0,0 +1,53 @@
if [ -z "$1" ]; then
echo "Error: Version argument is required"
echo "Usage: $0 <version>"
exit 1
fi
version="$1"
target_branch="PULL_REQUEST_${version}_android"
cd android
git clone https://github.com/Jellify-Music/App-Bundles.git
cd App-Bundles
if git ls-remote --exit-code --heads origin "$target_branch" >/dev/null 2>&1; then
echo "Branch '$target_branch' already exists on remote."
git checkout "$target_branch"
else
echo "Branch '$target_branch' does not exist on remote. Attempting to create it..."
git checkout -b "$target_branch"
fi
cd ../..
yarn createBundle:android
cd android/App-Bundles
bash ../../scripts/getRandomVersion.sh
git add .
git commit -m "OTA-Update - $(date +'%b %d %H:%M')"
git push https://x-access-token:$SIGNING_REPO_PAT@github.com/Jellify-Music/App-Bundles.git "$target_branch"
cd ..
rm -rf App-Bundles
cd ..
target_branch="PULL_REQUEST_${version}_ios"
cd ios
rm -rf App-Bundles
git clone https://github.com/Jellify-Music/App-Bundles.git
cd App-Bundles
if git ls-remote --exit-code --heads origin "$target_branch" >/dev/null 2>&1; then
echo "Branch '$target_branch' already exists on remote."
git checkout "$target_branch"
else
echo "Branch '$target_branch' does not exist on remote. Attempting to create it..."
git checkout -b "$target_branch"
fi
rm -rf Readme.md
cd ../..
yarn createBundle:ios
cd ios/App-Bundles
bash ../../scripts/getRandomVersion.sh
git add .
git commit -m "OTA-Update - $(date +'%b %d %H:%M')"
git push https://x-access-token:$SIGNING_REPO_PAT@github.com/Jellify-Music/App-Bundles.git "$target_branch"
cd ..
rm -rf App-Bundles
cd ..

View File

@@ -12,7 +12,7 @@ import {
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'
import DeviceInfo from 'react-native-device-info'
import { OTA_UPDATE_ENABLED } from '../../configs/config'
import { githubOTA, OTAUpdateManager, reloadApp } from 'react-native-nitro-ota'
import { githubOTA, OTAUpdateManager, reloadApp, getStoredOtaVersion } from 'react-native-nitro-ota'
const version = DeviceInfo.getVersion()
@@ -24,6 +24,9 @@ const { downloadUrl, versionUrl } = githubOTA({
ref: gitBranch, // optional, defaults to 'main'
})
const otaVersion = getStoredOtaVersion()
const isPRUpdate = otaVersion ? otaVersion.startsWith('PULL_REQUEST') : false
const otaManager = new OTAUpdateManager(downloadUrl, versionUrl)
export const downloadUpdate = (showCatchAlert: boolean = false) => {
@@ -77,7 +80,7 @@ const GitUpdateModal = () => {
useEffect(() => {
console.log('OTA_UPDATE_ENABLED', OTA_UPDATE_ENABLED)
if (__DEV__ || !OTA_UPDATE_ENABLED) {
if (__DEV__ || !OTA_UPDATE_ENABLED || isPRUpdate) {
return
}
onCheckGitVersion()

View File

@@ -0,0 +1,26 @@
import React, { useEffect, useState } from 'react'
import { Platform, Alert } from 'react-native'
import { githubOTA, OTAUpdateManager, reloadApp } from 'react-native-nitro-ota'
export const downloadPRUpdate = (prNumber: number) => {
const gitBranch = `PULL_REQUEST_${prNumber}_${Platform.OS}`
const { downloadUrl, versionUrl } = githubOTA({
githubUrl: 'https://github.com/Jellify-Music/App-Bundles',
otaVersionPath: 'ota.version', // optional, defaults to 'ota.version'
ref: gitBranch, // optional, defaults to 'main'
})
const otaManager = new OTAUpdateManager(downloadUrl, versionUrl)
otaManager
.downloadUpdate()
.then(() => {
Alert.alert('Jellify has been updated with the PR', 'Restart to apply the changes', [
{ text: 'OK', onPress: () => reloadApp() },
{ text: 'Cancel', style: 'cancel' },
])
})
.catch((error) => {
Alert.alert('PR is not available or to be found')
})
}

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useState } from 'react'
import SignOut from './sign-out-button'
import { SettingsStackParamList } from '../../../screens/Settings/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
@@ -7,42 +7,109 @@ import { Text } from '../../Global/helpers/text'
import SettingsListGroup from './settings-list-group'
import HTTPS from '../../../constants/protocols'
import { useJellifyUser, useJellifyLibrary, useJellifyServer } from '../../../stores'
import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
import { useDeveloperOptionsEnabled, usePrId } from '../../../stores/settings/developer'
import { YStack, XStack } from 'tamagui'
import Input from '../../Global/helpers/input'
import Button from '../../Global/helpers/button'
import Icon from '../../Global/components/icon'
import { Alert } from 'react-native'
import { SettingsTabList } from '../types'
import { downloadPRUpdate } from '../../OtaUpdates/otaPR'
export default function AccountTab(): React.JSX.Element {
const [server] = useJellifyServer()
const [user] = useJellifyUser()
const [library] = useJellifyLibrary()
const [developerOptionsEnabled, setDeveloperOptionsEnabled] = useDeveloperOptionsEnabled()
const [prId, setPrId] = usePrId()
const [localPrId, setLocalPrId] = useState(prId)
const navigation = useNavigation<NativeStackNavigationProp<SettingsStackParamList>>()
const handleSubmitPr = () => {
if (localPrId.trim()) {
setPrId(localPrId.trim())
downloadPRUpdate(Number(localPrId.trim()))
} else {
Alert.alert('Error', 'Please enter a valid PR ID')
}
}
const settingsList: SettingsTabList = [
{
title: 'Username',
subTitle: 'You are awesome!',
iconName: 'account-music',
iconColor: '$borderColor',
children: <Text>{user?.name ?? 'Unknown User'}</Text>,
},
{
title: 'Selected Library',
subTitle: 'Tap to change library',
iconName: 'book-music',
iconColor: '$borderColor',
children: <Text>{library?.musicLibraryName ?? 'Unknown Library'}</Text>,
onPress: () => navigation.navigate('LibrarySelection'),
},
{
title: server?.name ?? 'Untitled Server',
subTitle: server?.version ?? 'Unknown Jellyfin Version',
iconName: server?.url.includes(HTTPS) ? 'lock' : 'lock-open',
iconColor: server?.url.includes(HTTPS) ? '$success' : '$borderColor',
children: <Text>{server?.address ?? 'Unknown Server'}</Text>,
},
{
title: 'Developer Options',
subTitle: 'Enable advanced developer features',
iconName: developerOptionsEnabled ? 'code-braces' : 'code-braces-box',
iconColor: developerOptionsEnabled ? '$success' : '$borderColor',
children: (
<YStack gap='$2'>
<SwitchWithLabel
checked={developerOptionsEnabled}
onCheckedChange={setDeveloperOptionsEnabled}
size={'$2'}
label={developerOptionsEnabled ? 'Enabled' : 'Disabled'}
/>
{developerOptionsEnabled && (
<YStack gap='$3' paddingTop='$2'>
<Text color='$borderColor'>
Enter PR ID to test pull request builds
</Text>
<XStack gap='$2' alignItems='center' width='80%'>
<Input
flex={1}
placeholder='Enter PR ID'
value={localPrId}
onChangeText={setLocalPrId}
keyboardType='numeric'
size='$3'
/>
<Button
size='$3'
backgroundColor='$primary'
color='$background'
onPress={handleSubmitPr}
circular
icon={<Icon name='check' color='$background' small />}
/>
</XStack>
{prId && (
<Text color='$success' fontSize={'$2'}>
{`Current PR ID: ${prId}`}
</Text>
)}
</YStack>
)}
</YStack>
),
},
]
return (
<>
<SettingsListGroup
settingsList={[
{
title: 'Username',
subTitle: 'You are awesome!',
iconName: 'account-music',
iconColor: '$borderColor',
children: <Text>{user?.name ?? 'Unknown User'}</Text>,
},
{
title: 'Selected Library',
subTitle: 'Tap to change library',
iconName: 'book-music',
iconColor: '$borderColor',
children: <Text>{library?.musicLibraryName ?? 'Unknown Library'}</Text>,
onPress: () => navigation.navigate('LibrarySelection'),
},
{
title: server?.name ?? 'Untitled Server',
subTitle: server?.version ?? 'Unknown Jellyfin Version',
iconName: server?.url.includes(HTTPS) ? 'lock' : 'lock-open',
iconColor: server?.url.includes(HTTPS) ? '$success' : '$borderColor',
children: <Text>{server?.address ?? 'Unknown Server'}</Text>,
},
]}
/>
<SettingsListGroup settingsList={settingsList} />
<SignOut navigation={navigation} />
</>
)

View File

@@ -0,0 +1,46 @@
import { mmkvStateStorage } from '../../constants/storage'
import { create } from 'zustand'
import { createJSONStorage, devtools, persist } from 'zustand/middleware'
type DeveloperSettingsStore = {
developerOptionsEnabled: boolean
setDeveloperOptionsEnabled: (enabled: boolean) => void
prId: string
setPrId: (prId: string) => void
}
export const useDeveloperSettingsStore = create<DeveloperSettingsStore>()(
devtools(
persist(
(set) => ({
developerOptionsEnabled: false,
setDeveloperOptionsEnabled: (developerOptionsEnabled) =>
set({ developerOptionsEnabled }),
prId: '',
setPrId: (prId) => set({ prId }),
}),
{
name: 'developer-settings-storage',
storage: createJSONStorage(() => mmkvStateStorage),
},
),
),
)
export const useDeveloperOptionsEnabled: () => [boolean, (enabled: boolean) => void] = () => {
const developerOptionsEnabled = useDeveloperSettingsStore(
(state) => state.developerOptionsEnabled,
)
const setDeveloperOptionsEnabled = useDeveloperSettingsStore(
(state) => state.setDeveloperOptionsEnabled,
)
return [developerOptionsEnabled, setDeveloperOptionsEnabled]
}
export const usePrId: () => [string, (prId: string) => void] = () => {
const prId = useDeveloperSettingsStore((state) => state.prId)
const setPrId = useDeveloperSettingsStore((state) => state.setPrId)
return [prId, setPrId]
}