Repository files navigation

react-native-github-ota

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Downloads JS bundles from a GitHub Release (ota-latest tag) and applies them on next app restart — no app-store review needed.

How It Works

  1. A GitHub Action builds your JS bundle on every push to main and uploads it to a GitHub Release tagged ota-latest.
  2. This library compares the current build commit with the remote branch using the GitHub API.
  3. When an update is available, it downloads the bundle to the device and prompts the user to restart.
  4. On next launch, your native bootstrap code loads the downloaded bundle instead of the built-in one.

Installation

npm install react-native-github-ota
# or
yarn add react-native-github-ota

Peer Dependencies

Make sure these are installed in your project:

  • expo-file-system (>= 16)
  • react (>= 18)
  • react-native (>= 0.72)

Quick Start

Run the init command to set everything up automatically:

npx github-ota-init

This will:

  1. Copy the GitHub Actions workflow into .github/workflows/ota-bundle.yml
  2. Add embed-commit and prebuild scripts to your package.json
  3. Add the config plugin to your app.json — native code is applied automatically on expo prebuild
  4. For bare RN projects (no Expo): patches MainApplication.kt directly

After running init, rebuild your native project:

npx expo prebuild --clean

Native Code — Handled Automatically

The library ships an Expo Config Plugin that automatically patches MainApplication.kt during expo prebuild. No manual native code changes needed.

For Expo projects: Just add the plugin to your app.json (the init command does this for you):

{
"expo": {
"plugins": ["react-native-github-ota"]
}
}

For bare React Native projects: The init command patches MainApplication.kt directly. If you need to do it manually, add this:

importjava.io.File// Inside your DefaultReactNativeHost object, add:overridefungetJSBundleFile(): String? {
val otaBundle =File(applicationContext.filesDir, "ota/index.android.bundle")
if (!otaBundle.exists()) returnnull// If the APK was updated after the OTA bundle was downloaded, the OTA is staletry {
val appInfo = applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0)
if (appInfo.lastUpdateTime > otaBundle.lastModified()) {
otaBundle.delete()
File(applicationContext.filesDir, "ota/meta.json").delete()
returnnull
}
} catch (_:Exception) {}
return otaBundle.absolutePath
}

This tells React Native: "if an OTA bundle exists on disk and is newer than the installed APK, load it; otherwise use the built-in one." This prevents stale OTA bundles from overriding a newer rebuild.

Manual Setup

If you prefer to set things up manually:

1. Embed Build Info (required)

Before each build, run the embed-commit script to stamp the current git SHA into your app:

node node_modules/react-native-github-ota/scripts/embed-commit.js

Or add it to your build scripts:

{
"scripts": {
"embed-commit": "node node_modules/react-native-github-ota/scripts/embed-commit.js",
"prebuild": "node node_modules/react-native-github-ota/scripts/embed-commit.js && npx expo prebuild"
}
}

The script creates/updates a constants/buildInfo.ts file in your project root.

2. Add the GitHub Actions Workflow

Copy the workflow template to your project:

mkdir -p .github/workflows
cp node_modules/react-native-github-ota/workflow/ota-bundle.yml .github/workflows/

Or create .github/workflows/ota-bundle.yml manually — see workflow/ota-bundle.yml for the full template.

What the workflow does:

  • Triggers on every push to main
  • Installs dependencies and embeds the commit hash
  • Runs react-native bundle to create the JS bundle
  • Creates an ota-manifest.json with commit metadata
  • Uploads both files to a GitHub Release tagged ota-latest

Note: The workflow uses GITHUB_TOKEN which is automatically provided by GitHub Actions — no extra secrets needed.

Usage

Configure

Call configureOta once at app startup (e.g. in your root layout):

import{configureOta}from"react-native-github-ota";configureOta({owner: "your-github-username",repo: "your-repo-name",branch: "main",// optional, default "main"releaseTag: "ota-latest",// optional, default "ota-latest"bundleFileName: "index.android.bundle",// optional});

Hook

import{useGithubOta}from"react-native-github-ota";import{BUILD_INFO}from"./constants/buildInfo";functionMyComponent(){const{
status,
error,
updateInfo,
isChecking,
isDownloading,
checkUpdate,
downloadAndApplyUpdate,
buildInfo,}=useGithubOta({autoCheckOnMount: true,buildInfo: BUILD_INFO,// Optional: control auto-check with your own settingsshouldAutoCheck: async()=>{// e.g. read from AsyncStoragereturntrue;},});// ...}

Pre-built Banner

import{OtaUpdateBanner}from"react-native-github-ota";<OtaUpdateBannervisible={bannerVisible}status={status}error={error}updateInfo={updateInfo}isDownloading={isDownloading}onDownload={downloadAndApplyUpdate}onClose={()=>setBannerVisible(false)}// Optional theme overrides:colors={{surface: "#1a1a1a",text: "#ffffff",textSecondary: "#a1a1aa",primary: "#3b82f6",danger: "#ef4444",border: "#27272a",}}/>;

API

configureOta(config)

OptionTypeDefaultDescription
ownerstringrequiredGitHub repository owner
repostringrequiredGitHub repository name
branchstring"main"Branch to compare against
releaseTagstring"ota-latest"Release tag containing the OTA bundle
bundleFileNamestring"index.android.bundle"Bundle asset filename in the release
autoSettingsKeystring"@github_ota_settings"AsyncStorage key for settings

useGithubOta(options)

OptionTypeDefaultDescription
buildInfoBuildInforequiredBuild info from embed-commit script
autoCheckOnMountbooleantrueAuto-check for updates on mount
shouldAutoCheck() => boolean | Promise<boolean>Guard for auto-check (e.g. user settings)

Returns:

FieldTypeDescription
statusstring | nullCurrent status message
errorstring | nullError message if any
isAvailablebooleanWhether an update is available
isCheckingbooleanCurrently checking for updates
isDownloadingbooleanCurrently downloading
downloadProgressnumberDownload progress (0-100)
updateInfoUpdateInfo | nullDetails about the available update
lastCheckednumber | nullTimestamp of last check
checkUpdate(manual?: boolean) => voidTrigger an update check
downloadAndApplyUpdate() => voidDownload and install the update
setStatus(s: string | null) => voidOverride status message
buildInfoBuildInfoThe build info passed in

OtaUpdateBanner

A ready-made banner component. Accepts theme colors for dark/light mode support.

npx github-ota-init

CLI command that scaffolds the GitHub Actions workflow, build scripts, config plugin, and native setup into your project. For Expo projects it adds the config plugin to app.json; for bare RN projects it patches MainApplication.kt directly.

License

MIT

About

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

react-native-github-ota

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Downloads JS bundles from a GitHub Release (ota-latest tag) and applies them on next app restart — no app-store review needed.

How It Works

  1. A GitHub Action builds your JS bundle on every push to main and uploads it to a GitHub Release tagged ota-latest.
  2. This library compares the current build commit with the remote branch using the GitHub API.
  3. When an update is available, it downloads the bundle to the device and prompts the user to restart.
  4. On next launch, your native bootstrap code loads the downloaded bundle instead of the built-in one.

Installation

npm install react-native-github-ota
# or
yarn add react-native-github-ota

Peer Dependencies

Make sure these are installed in your project:

  • expo-file-system (>= 16)
  • react (>= 18)
  • react-native (>= 0.72)

Quick Start

Run the init command to set everything up automatically:

npx github-ota-init

This will:

  1. Copy the GitHub Actions workflow into .github/workflows/ota-bundle.yml
  2. Add embed-commit and prebuild scripts to your package.json
  3. Add the config plugin to your app.json — native code is applied automatically on expo prebuild
  4. For bare RN projects (no Expo): patches MainApplication.kt directly

After running init, rebuild your native project:

npx expo prebuild --clean

Native Code — Handled Automatically

The library ships an Expo Config Plugin that automatically patches MainApplication.kt during expo prebuild. No manual native code changes needed.

For Expo projects: Just add the plugin to your app.json (the init command does this for you):

{
"expo": {
"plugins": ["react-native-github-ota"]
}
}

For bare React Native projects: The init command patches MainApplication.kt directly. If you need to do it manually, add this:

importjava.io.File// Inside your DefaultReactNativeHost object, add:overridefungetJSBundleFile(): String? {
val otaBundle =File(applicationContext.filesDir, "ota/index.android.bundle")
if (!otaBundle.exists()) returnnull// If the APK was updated after the OTA bundle was downloaded, the OTA is staletry {
val appInfo = applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0)
if (appInfo.lastUpdateTime > otaBundle.lastModified()) {
otaBundle.delete()
File(applicationContext.filesDir, "ota/meta.json").delete()
returnnull
}
} catch (_:Exception) {}
return otaBundle.absolutePath
}

This tells React Native: "if an OTA bundle exists on disk and is newer than the installed APK, load it; otherwise use the built-in one." This prevents stale OTA bundles from overriding a newer rebuild.

Manual Setup

If you prefer to set things up manually:

1. Embed Build Info (required)

Before each build, run the embed-commit script to stamp the current git SHA into your app:

node node_modules/react-native-github-ota/scripts/embed-commit.js

Or add it to your build scripts:

{
"scripts": {
"embed-commit": "node node_modules/react-native-github-ota/scripts/embed-commit.js",
"prebuild": "node node_modules/react-native-github-ota/scripts/embed-commit.js && npx expo prebuild"
}
}

The script creates/updates a constants/buildInfo.ts file in your project root.

2. Add the GitHub Actions Workflow

Copy the workflow template to your project:

mkdir -p .github/workflows
cp node_modules/react-native-github-ota/workflow/ota-bundle.yml .github/workflows/

Or create .github/workflows/ota-bundle.yml manually — see workflow/ota-bundle.yml for the full template.

What the workflow does:

  • Triggers on every push to main
  • Installs dependencies and embeds the commit hash
  • Runs react-native bundle to create the JS bundle
  • Creates an ota-manifest.json with commit metadata
  • Uploads both files to a GitHub Release tagged ota-latest

Note: The workflow uses GITHUB_TOKEN which is automatically provided by GitHub Actions — no extra secrets needed.

Usage

Configure

Call configureOta once at app startup (e.g. in your root layout):

import{configureOta}from"react-native-github-ota";configureOta({owner: "your-github-username",repo: "your-repo-name",branch: "main",// optional, default "main"releaseTag: "ota-latest",// optional, default "ota-latest"bundleFileName: "index.android.bundle",// optional});

Hook

import{useGithubOta}from"react-native-github-ota";import{BUILD_INFO}from"./constants/buildInfo";functionMyComponent(){const{
status,
error,
updateInfo,
isChecking,
isDownloading,
checkUpdate,
downloadAndApplyUpdate,
buildInfo,}=useGithubOta({autoCheckOnMount: true,buildInfo: BUILD_INFO,// Optional: control auto-check with your own settingsshouldAutoCheck: async()=>{// e.g. read from AsyncStoragereturntrue;},});// ...}

Pre-built Banner

import{OtaUpdateBanner}from"react-native-github-ota";<OtaUpdateBannervisible={bannerVisible}status={status}error={error}updateInfo={updateInfo}isDownloading={isDownloading}onDownload={downloadAndApplyUpdate}onClose={()=>setBannerVisible(false)}// Optional theme overrides:colors={{surface: "#1a1a1a",text: "#ffffff",textSecondary: "#a1a1aa",primary: "#3b82f6",danger: "#ef4444",border: "#27272a",}}/>;

API

configureOta(config)

OptionTypeDefaultDescription
ownerstringrequiredGitHub repository owner
repostringrequiredGitHub repository name
branchstring"main"Branch to compare against
releaseTagstring"ota-latest"Release tag containing the OTA bundle
bundleFileNamestring"index.android.bundle"Bundle asset filename in the release
autoSettingsKeystring"@github_ota_settings"AsyncStorage key for settings

useGithubOta(options)

OptionTypeDefaultDescription
buildInfoBuildInforequiredBuild info from embed-commit script
autoCheckOnMountbooleantrueAuto-check for updates on mount
shouldAutoCheck() => boolean | Promise<boolean>Guard for auto-check (e.g. user settings)

Returns:

FieldTypeDescription
statusstring | nullCurrent status message
errorstring | nullError message if any
isAvailablebooleanWhether an update is available
isCheckingbooleanCurrently checking for updates
isDownloadingbooleanCurrently downloading
downloadProgressnumberDownload progress (0-100)
updateInfoUpdateInfo | nullDetails about the available update
lastCheckednumber | nullTimestamp of last check
checkUpdate(manual?: boolean) => voidTrigger an update check
downloadAndApplyUpdate() => voidDownload and install the update
setStatus(s: string | null) => voidOverride status message
buildInfoBuildInfoThe build info passed in

OtaUpdateBanner

A ready-made banner component. Accepts theme colors for dark/light mode support.

npx github-ota-init

CLI command that scaffolds the GitHub Actions workflow, build scripts, config plugin, and native setup into your project. For Expo projects it adds the config plugin to app.json; for bare RN projects it patches MainApplication.kt directly.

License

MIT

About

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

react-native-github-ota

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Downloads JS bundles from a GitHub Release (ota-latest tag) and applies them on next app restart — no app-store review needed.

How It Works

  1. A GitHub Action builds your JS bundle on every push to main and uploads it to a GitHub Release tagged ota-latest.
  2. This library compares the current build commit with the remote branch using the GitHub API.
  3. When an update is available, it downloads the bundle to the device and prompts the user to restart.
  4. On next launch, your native bootstrap code loads the downloaded bundle instead of the built-in one.

Installation

npm install react-native-github-ota
# or
yarn add react-native-github-ota

Peer Dependencies

Make sure these are installed in your project:

  • expo-file-system (>= 16)
  • react (>= 18)
  • react-native (>= 0.72)

Quick Start

Run the init command to set everything up automatically:

npx github-ota-init

This will:

  1. Copy the GitHub Actions workflow into .github/workflows/ota-bundle.yml
  2. Add embed-commit and prebuild scripts to your package.json
  3. Add the config plugin to your app.json — native code is applied automatically on expo prebuild
  4. For bare RN projects (no Expo): patches MainApplication.kt directly

After running init, rebuild your native project:

npx expo prebuild --clean

Native Code — Handled Automatically

The library ships an Expo Config Plugin that automatically patches MainApplication.kt during expo prebuild. No manual native code changes needed.

For Expo projects: Just add the plugin to your app.json (the init command does this for you):

{
"expo": {
"plugins": ["react-native-github-ota"]
}
}

For bare React Native projects: The init command patches MainApplication.kt directly. If you need to do it manually, add this:

importjava.io.File// Inside your DefaultReactNativeHost object, add:overridefungetJSBundleFile(): String? {
val otaBundle =File(applicationContext.filesDir, "ota/index.android.bundle")
if (!otaBundle.exists()) returnnull// If the APK was updated after the OTA bundle was downloaded, the OTA is staletry {
val appInfo = applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0)
if (appInfo.lastUpdateTime > otaBundle.lastModified()) {
otaBundle.delete()
File(applicationContext.filesDir, "ota/meta.json").delete()
returnnull
}
} catch (_:Exception) {}
return otaBundle.absolutePath
}

This tells React Native: "if an OTA bundle exists on disk and is newer than the installed APK, load it; otherwise use the built-in one." This prevents stale OTA bundles from overriding a newer rebuild.

Manual Setup

If you prefer to set things up manually:

1. Embed Build Info (required)

Before each build, run the embed-commit script to stamp the current git SHA into your app:

node node_modules/react-native-github-ota/scripts/embed-commit.js

Or add it to your build scripts:

{
"scripts": {
"embed-commit": "node node_modules/react-native-github-ota/scripts/embed-commit.js",
"prebuild": "node node_modules/react-native-github-ota/scripts/embed-commit.js && npx expo prebuild"
}
}

The script creates/updates a constants/buildInfo.ts file in your project root.

2. Add the GitHub Actions Workflow

Copy the workflow template to your project:

mkdir -p .github/workflows
cp node_modules/react-native-github-ota/workflow/ota-bundle.yml .github/workflows/

Or create .github/workflows/ota-bundle.yml manually — see workflow/ota-bundle.yml for the full template.

What the workflow does:

  • Triggers on every push to main
  • Installs dependencies and embeds the commit hash
  • Runs react-native bundle to create the JS bundle
  • Creates an ota-manifest.json with commit metadata
  • Uploads both files to a GitHub Release tagged ota-latest

Note: The workflow uses GITHUB_TOKEN which is automatically provided by GitHub Actions — no extra secrets needed.

Usage

Configure

Call configureOta once at app startup (e.g. in your root layout):

import{configureOta}from"react-native-github-ota";configureOta({owner: "your-github-username",repo: "your-repo-name",branch: "main",// optional, default "main"releaseTag: "ota-latest",// optional, default "ota-latest"bundleFileName: "index.android.bundle",// optional});

Hook

import{useGithubOta}from"react-native-github-ota";import{BUILD_INFO}from"./constants/buildInfo";functionMyComponent(){const{
status,
error,
updateInfo,
isChecking,
isDownloading,
checkUpdate,
downloadAndApplyUpdate,
buildInfo,}=useGithubOta({autoCheckOnMount: true,buildInfo: BUILD_INFO,// Optional: control auto-check with your own settingsshouldAutoCheck: async()=>{// e.g. read from AsyncStoragereturntrue;},});// ...}

Pre-built Banner

import{OtaUpdateBanner}from"react-native-github-ota";<OtaUpdateBannervisible={bannerVisible}status={status}error={error}updateInfo={updateInfo}isDownloading={isDownloading}onDownload={downloadAndApplyUpdate}onClose={()=>setBannerVisible(false)}// Optional theme overrides:colors={{surface: "#1a1a1a",text: "#ffffff",textSecondary: "#a1a1aa",primary: "#3b82f6",danger: "#ef4444",border: "#27272a",}}/>;

API

configureOta(config)

OptionTypeDefaultDescription
ownerstringrequiredGitHub repository owner
repostringrequiredGitHub repository name
branchstring"main"Branch to compare against
releaseTagstring"ota-latest"Release tag containing the OTA bundle
bundleFileNamestring"index.android.bundle"Bundle asset filename in the release
autoSettingsKeystring"@github_ota_settings"AsyncStorage key for settings

useGithubOta(options)

OptionTypeDefaultDescription
buildInfoBuildInforequiredBuild info from embed-commit script
autoCheckOnMountbooleantrueAuto-check for updates on mount
shouldAutoCheck() => boolean | Promise<boolean>Guard for auto-check (e.g. user settings)

Returns:

FieldTypeDescription
statusstring | nullCurrent status message
errorstring | nullError message if any
isAvailablebooleanWhether an update is available
isCheckingbooleanCurrently checking for updates
isDownloadingbooleanCurrently downloading
downloadProgressnumberDownload progress (0-100)
updateInfoUpdateInfo | nullDetails about the available update
lastCheckednumber | nullTimestamp of last check
checkUpdate(manual?: boolean) => voidTrigger an update check
downloadAndApplyUpdate() => voidDownload and install the update
setStatus(s: string | null) => voidOverride status message
buildInfoBuildInfoThe build info passed in

OtaUpdateBanner

A ready-made banner component. Accepts theme colors for dark/light mode support.

npx github-ota-init

CLI command that scaffolds the GitHub Actions workflow, build scripts, config plugin, and native setup into your project. For Expo projects it adds the config plugin to app.json; for bare RN projects it patches MainApplication.kt directly.

License

MIT

About

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

react-native-github-ota

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Downloads JS bundles from a GitHub Release (ota-latest tag) and applies them on next app restart — no app-store review needed.

How It Works

  1. A GitHub Action builds your JS bundle on every push to main and uploads it to a GitHub Release tagged ota-latest.
  2. This library compares the current build commit with the remote branch using the GitHub API.
  3. When an update is available, it downloads the bundle to the device and prompts the user to restart.
  4. On next launch, your native bootstrap code loads the downloaded bundle instead of the built-in one.

Installation

npm install react-native-github-ota
# or
yarn add react-native-github-ota

Peer Dependencies

Make sure these are installed in your project:

  • expo-file-system (>= 16)
  • react (>= 18)
  • react-native (>= 0.72)

Quick Start

Run the init command to set everything up automatically:

npx github-ota-init

This will:

  1. Copy the GitHub Actions workflow into .github/workflows/ota-bundle.yml
  2. Add embed-commit and prebuild scripts to your package.json
  3. Add the config plugin to your app.json — native code is applied automatically on expo prebuild
  4. For bare RN projects (no Expo): patches MainApplication.kt directly

After running init, rebuild your native project:

npx expo prebuild --clean

Native Code — Handled Automatically

The library ships an Expo Config Plugin that automatically patches MainApplication.kt during expo prebuild. No manual native code changes needed.

For Expo projects: Just add the plugin to your app.json (the init command does this for you):

{
"expo": {
"plugins": ["react-native-github-ota"]
}
}

For bare React Native projects: The init command patches MainApplication.kt directly. If you need to do it manually, add this:

importjava.io.File// Inside your DefaultReactNativeHost object, add:overridefungetJSBundleFile(): String? {
val otaBundle =File(applicationContext.filesDir, "ota/index.android.bundle")
if (!otaBundle.exists()) returnnull// If the APK was updated after the OTA bundle was downloaded, the OTA is staletry {
val appInfo = applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0)
if (appInfo.lastUpdateTime > otaBundle.lastModified()) {
otaBundle.delete()
File(applicationContext.filesDir, "ota/meta.json").delete()
returnnull
}
} catch (_:Exception) {}
return otaBundle.absolutePath
}

This tells React Native: "if an OTA bundle exists on disk and is newer than the installed APK, load it; otherwise use the built-in one." This prevents stale OTA bundles from overriding a newer rebuild.

Manual Setup

If you prefer to set things up manually:

1. Embed Build Info (required)

Before each build, run the embed-commit script to stamp the current git SHA into your app:

node node_modules/react-native-github-ota/scripts/embed-commit.js

Or add it to your build scripts:

{
"scripts": {
"embed-commit": "node node_modules/react-native-github-ota/scripts/embed-commit.js",
"prebuild": "node node_modules/react-native-github-ota/scripts/embed-commit.js && npx expo prebuild"
}
}

The script creates/updates a constants/buildInfo.ts file in your project root.

2. Add the GitHub Actions Workflow

Copy the workflow template to your project:

mkdir -p .github/workflows
cp node_modules/react-native-github-ota/workflow/ota-bundle.yml .github/workflows/

Or create .github/workflows/ota-bundle.yml manually — see workflow/ota-bundle.yml for the full template.

What the workflow does:

  • Triggers on every push to main
  • Installs dependencies and embeds the commit hash
  • Runs react-native bundle to create the JS bundle
  • Creates an ota-manifest.json with commit metadata
  • Uploads both files to a GitHub Release tagged ota-latest

Note: The workflow uses GITHUB_TOKEN which is automatically provided by GitHub Actions — no extra secrets needed.

Usage

Configure

Call configureOta once at app startup (e.g. in your root layout):

import{configureOta}from"react-native-github-ota";configureOta({owner: "your-github-username",repo: "your-repo-name",branch: "main",// optional, default "main"releaseTag: "ota-latest",// optional, default "ota-latest"bundleFileName: "index.android.bundle",// optional});

Hook

import{useGithubOta}from"react-native-github-ota";import{BUILD_INFO}from"./constants/buildInfo";functionMyComponent(){const{
status,
error,
updateInfo,
isChecking,
isDownloading,
checkUpdate,
downloadAndApplyUpdate,
buildInfo,}=useGithubOta({autoCheckOnMount: true,buildInfo: BUILD_INFO,// Optional: control auto-check with your own settingsshouldAutoCheck: async()=>{// e.g. read from AsyncStoragereturntrue;},});// ...}

Pre-built Banner

import{OtaUpdateBanner}from"react-native-github-ota";<OtaUpdateBannervisible={bannerVisible}status={status}error={error}updateInfo={updateInfo}isDownloading={isDownloading}onDownload={downloadAndApplyUpdate}onClose={()=>setBannerVisible(false)}// Optional theme overrides:colors={{surface: "#1a1a1a",text: "#ffffff",textSecondary: "#a1a1aa",primary: "#3b82f6",danger: "#ef4444",border: "#27272a",}}/>;

API

configureOta(config)

OptionTypeDefaultDescription
ownerstringrequiredGitHub repository owner
repostringrequiredGitHub repository name
branchstring"main"Branch to compare against
releaseTagstring"ota-latest"Release tag containing the OTA bundle
bundleFileNamestring"index.android.bundle"Bundle asset filename in the release
autoSettingsKeystring"@github_ota_settings"AsyncStorage key for settings

useGithubOta(options)

OptionTypeDefaultDescription
buildInfoBuildInforequiredBuild info from embed-commit script
autoCheckOnMountbooleantrueAuto-check for updates on mount
shouldAutoCheck() => boolean | Promise<boolean>Guard for auto-check (e.g. user settings)

Returns:

FieldTypeDescription
statusstring | nullCurrent status message
errorstring | nullError message if any
isAvailablebooleanWhether an update is available
isCheckingbooleanCurrently checking for updates
isDownloadingbooleanCurrently downloading
downloadProgressnumberDownload progress (0-100)
updateInfoUpdateInfo | nullDetails about the available update
lastCheckednumber | nullTimestamp of last check
checkUpdate(manual?: boolean) => voidTrigger an update check
downloadAndApplyUpdate() => voidDownload and install the update
setStatus(s: string | null) => voidOverride status message
buildInfoBuildInfoThe build info passed in

OtaUpdateBanner

A ready-made banner component. Accepts theme colors for dark/light mode support.

npx github-ota-init

CLI command that scaffolds the GitHub Actions workflow, build scripts, config plugin, and native setup into your project. For Expo projects it adds the config plugin to app.json; for bare RN projects it patches MainApplication.kt directly.

License

MIT

About

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

react-native-github-ota

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Downloads JS bundles from a GitHub Release (ota-latest tag) and applies them on next app restart — no app-store review needed.

How It Works

  1. A GitHub Action builds your JS bundle on every push to main and uploads it to a GitHub Release tagged ota-latest.
  2. This library compares the current build commit with the remote branch using the GitHub API.
  3. When an update is available, it downloads the bundle to the device and prompts the user to restart.
  4. On next launch, your native bootstrap code loads the downloaded bundle instead of the built-in one.

Installation

npm install react-native-github-ota
# or
yarn add react-native-github-ota

Peer Dependencies

Make sure these are installed in your project:

  • expo-file-system (>= 16)
  • react (>= 18)
  • react-native (>= 0.72)

Quick Start

Run the init command to set everything up automatically:

npx github-ota-init

This will:

  1. Copy the GitHub Actions workflow into .github/workflows/ota-bundle.yml
  2. Add embed-commit and prebuild scripts to your package.json
  3. Add the config plugin to your app.json — native code is applied automatically on expo prebuild
  4. For bare RN projects (no Expo): patches MainApplication.kt directly

After running init, rebuild your native project:

npx expo prebuild --clean

Native Code — Handled Automatically

The library ships an Expo Config Plugin that automatically patches MainApplication.kt during expo prebuild. No manual native code changes needed.

For Expo projects: Just add the plugin to your app.json (the init command does this for you):

{
"expo": {
"plugins": ["react-native-github-ota"]
}
}

For bare React Native projects: The init command patches MainApplication.kt directly. If you need to do it manually, add this:

importjava.io.File// Inside your DefaultReactNativeHost object, add:overridefungetJSBundleFile(): String? {
val otaBundle =File(applicationContext.filesDir, "ota/index.android.bundle")
if (!otaBundle.exists()) returnnull// If the APK was updated after the OTA bundle was downloaded, the OTA is staletry {
val appInfo = applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0)
if (appInfo.lastUpdateTime > otaBundle.lastModified()) {
otaBundle.delete()
File(applicationContext.filesDir, "ota/meta.json").delete()
returnnull
}
} catch (_:Exception) {}
return otaBundle.absolutePath
}

This tells React Native: "if an OTA bundle exists on disk and is newer than the installed APK, load it; otherwise use the built-in one." This prevents stale OTA bundles from overriding a newer rebuild.

Manual Setup

If you prefer to set things up manually:

1. Embed Build Info (required)

Before each build, run the embed-commit script to stamp the current git SHA into your app:

node node_modules/react-native-github-ota/scripts/embed-commit.js

Or add it to your build scripts:

{
"scripts": {
"embed-commit": "node node_modules/react-native-github-ota/scripts/embed-commit.js",
"prebuild": "node node_modules/react-native-github-ota/scripts/embed-commit.js && npx expo prebuild"
}
}

The script creates/updates a constants/buildInfo.ts file in your project root.

2. Add the GitHub Actions Workflow

Copy the workflow template to your project:

mkdir -p .github/workflows
cp node_modules/react-native-github-ota/workflow/ota-bundle.yml .github/workflows/

Or create .github/workflows/ota-bundle.yml manually — see workflow/ota-bundle.yml for the full template.

What the workflow does:

  • Triggers on every push to main
  • Installs dependencies and embeds the commit hash
  • Runs react-native bundle to create the JS bundle
  • Creates an ota-manifest.json with commit metadata
  • Uploads both files to a GitHub Release tagged ota-latest

Note: The workflow uses GITHUB_TOKEN which is automatically provided by GitHub Actions — no extra secrets needed.

Usage

Configure

Call configureOta once at app startup (e.g. in your root layout):

import{configureOta}from"react-native-github-ota";configureOta({owner: "your-github-username",repo: "your-repo-name",branch: "main",// optional, default "main"releaseTag: "ota-latest",// optional, default "ota-latest"bundleFileName: "index.android.bundle",// optional});

Hook

import{useGithubOta}from"react-native-github-ota";import{BUILD_INFO}from"./constants/buildInfo";functionMyComponent(){const{
status,
error,
updateInfo,
isChecking,
isDownloading,
checkUpdate,
downloadAndApplyUpdate,
buildInfo,}=useGithubOta({autoCheckOnMount: true,buildInfo: BUILD_INFO,// Optional: control auto-check with your own settingsshouldAutoCheck: async()=>{// e.g. read from AsyncStoragereturntrue;},});// ...}

Pre-built Banner

import{OtaUpdateBanner}from"react-native-github-ota";<OtaUpdateBannervisible={bannerVisible}status={status}error={error}updateInfo={updateInfo}isDownloading={isDownloading}onDownload={downloadAndApplyUpdate}onClose={()=>setBannerVisible(false)}// Optional theme overrides:colors={{surface: "#1a1a1a",text: "#ffffff",textSecondary: "#a1a1aa",primary: "#3b82f6",danger: "#ef4444",border: "#27272a",}}/>;

API

configureOta(config)

OptionTypeDefaultDescription
ownerstringrequiredGitHub repository owner
repostringrequiredGitHub repository name
branchstring"main"Branch to compare against
releaseTagstring"ota-latest"Release tag containing the OTA bundle
bundleFileNamestring"index.android.bundle"Bundle asset filename in the release
autoSettingsKeystring"@github_ota_settings"AsyncStorage key for settings

useGithubOta(options)

OptionTypeDefaultDescription
buildInfoBuildInforequiredBuild info from embed-commit script
autoCheckOnMountbooleantrueAuto-check for updates on mount
shouldAutoCheck() => boolean | Promise<boolean>Guard for auto-check (e.g. user settings)

Returns:

FieldTypeDescription
statusstring | nullCurrent status message
errorstring | nullError message if any
isAvailablebooleanWhether an update is available
isCheckingbooleanCurrently checking for updates
isDownloadingbooleanCurrently downloading
downloadProgressnumberDownload progress (0-100)
updateInfoUpdateInfo | nullDetails about the available update
lastCheckednumber | nullTimestamp of last check
checkUpdate(manual?: boolean) => voidTrigger an update check
downloadAndApplyUpdate() => voidDownload and install the update
setStatus(s: string | null) => voidOverride status message
buildInfoBuildInfoThe build info passed in

OtaUpdateBanner

A ready-made banner component. Accepts theme colors for dark/light mode support.

npx github-ota-init

CLI command that scaffolds the GitHub Actions workflow, build scripts, config plugin, and native setup into your project. For Expo projects it adds the config plugin to app.json; for bare RN projects it patches MainApplication.kt directly.

License

MIT

About

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

react-native-github-ota

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Downloads JS bundles from a GitHub Release (ota-latest tag) and applies them on next app restart — no app-store review needed.

How It Works

  1. A GitHub Action builds your JS bundle on every push to main and uploads it to a GitHub Release tagged ota-latest.
  2. This library compares the current build commit with the remote branch using the GitHub API.
  3. When an update is available, it downloads the bundle to the device and prompts the user to restart.
  4. On next launch, your native bootstrap code loads the downloaded bundle instead of the built-in one.

Installation

npm install react-native-github-ota
# or
yarn add react-native-github-ota

Peer Dependencies

Make sure these are installed in your project:

  • expo-file-system (>= 16)
  • react (>= 18)
  • react-native (>= 0.72)

Quick Start

Run the init command to set everything up automatically:

npx github-ota-init

This will:

  1. Copy the GitHub Actions workflow into .github/workflows/ota-bundle.yml
  2. Add embed-commit and prebuild scripts to your package.json
  3. Add the config plugin to your app.json — native code is applied automatically on expo prebuild
  4. For bare RN projects (no Expo): patches MainApplication.kt directly

After running init, rebuild your native project:

npx expo prebuild --clean

Native Code — Handled Automatically

The library ships an Expo Config Plugin that automatically patches MainApplication.kt during expo prebuild. No manual native code changes needed.

For Expo projects: Just add the plugin to your app.json (the init command does this for you):

{
"expo": {
"plugins": ["react-native-github-ota"]
}
}

For bare React Native projects: The init command patches MainApplication.kt directly. If you need to do it manually, add this:

importjava.io.File// Inside your DefaultReactNativeHost object, add:overridefungetJSBundleFile(): String? {
val otaBundle =File(applicationContext.filesDir, "ota/index.android.bundle")
if (!otaBundle.exists()) returnnull// If the APK was updated after the OTA bundle was downloaded, the OTA is staletry {
val appInfo = applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0)
if (appInfo.lastUpdateTime > otaBundle.lastModified()) {
otaBundle.delete()
File(applicationContext.filesDir, "ota/meta.json").delete()
returnnull
}
} catch (_:Exception) {}
return otaBundle.absolutePath
}

This tells React Native: "if an OTA bundle exists on disk and is newer than the installed APK, load it; otherwise use the built-in one." This prevents stale OTA bundles from overriding a newer rebuild.

Manual Setup

If you prefer to set things up manually:

1. Embed Build Info (required)

Before each build, run the embed-commit script to stamp the current git SHA into your app:

node node_modules/react-native-github-ota/scripts/embed-commit.js

Or add it to your build scripts:

{
"scripts": {
"embed-commit": "node node_modules/react-native-github-ota/scripts/embed-commit.js",
"prebuild": "node node_modules/react-native-github-ota/scripts/embed-commit.js && npx expo prebuild"
}
}

The script creates/updates a constants/buildInfo.ts file in your project root.

2. Add the GitHub Actions Workflow

Copy the workflow template to your project:

mkdir -p .github/workflows
cp node_modules/react-native-github-ota/workflow/ota-bundle.yml .github/workflows/

Or create .github/workflows/ota-bundle.yml manually — see workflow/ota-bundle.yml for the full template.

What the workflow does:

  • Triggers on every push to main
  • Installs dependencies and embeds the commit hash
  • Runs react-native bundle to create the JS bundle
  • Creates an ota-manifest.json with commit metadata
  • Uploads both files to a GitHub Release tagged ota-latest

Note: The workflow uses GITHUB_TOKEN which is automatically provided by GitHub Actions — no extra secrets needed.

Usage

Configure

Call configureOta once at app startup (e.g. in your root layout):

import{configureOta}from"react-native-github-ota";configureOta({owner: "your-github-username",repo: "your-repo-name",branch: "main",// optional, default "main"releaseTag: "ota-latest",// optional, default "ota-latest"bundleFileName: "index.android.bundle",// optional});

Hook

import{useGithubOta}from"react-native-github-ota";import{BUILD_INFO}from"./constants/buildInfo";functionMyComponent(){const{
status,
error,
updateInfo,
isChecking,
isDownloading,
checkUpdate,
downloadAndApplyUpdate,
buildInfo,}=useGithubOta({autoCheckOnMount: true,buildInfo: BUILD_INFO,// Optional: control auto-check with your own settingsshouldAutoCheck: async()=>{// e.g. read from AsyncStoragereturntrue;},});// ...}

Pre-built Banner

import{OtaUpdateBanner}from"react-native-github-ota";<OtaUpdateBannervisible={bannerVisible}status={status}error={error}updateInfo={updateInfo}isDownloading={isDownloading}onDownload={downloadAndApplyUpdate}onClose={()=>setBannerVisible(false)}// Optional theme overrides:colors={{surface: "#1a1a1a",text: "#ffffff",textSecondary: "#a1a1aa",primary: "#3b82f6",danger: "#ef4444",border: "#27272a",}}/>;

API

configureOta(config)

OptionTypeDefaultDescription
ownerstringrequiredGitHub repository owner
repostringrequiredGitHub repository name
branchstring"main"Branch to compare against
releaseTagstring"ota-latest"Release tag containing the OTA bundle
bundleFileNamestring"index.android.bundle"Bundle asset filename in the release
autoSettingsKeystring"@github_ota_settings"AsyncStorage key for settings

useGithubOta(options)

OptionTypeDefaultDescription
buildInfoBuildInforequiredBuild info from embed-commit script
autoCheckOnMountbooleantrueAuto-check for updates on mount
shouldAutoCheck() => boolean | Promise<boolean>Guard for auto-check (e.g. user settings)

Returns:

FieldTypeDescription
statusstring | nullCurrent status message
errorstring | nullError message if any
isAvailablebooleanWhether an update is available
isCheckingbooleanCurrently checking for updates
isDownloadingbooleanCurrently downloading
downloadProgressnumberDownload progress (0-100)
updateInfoUpdateInfo | nullDetails about the available update
lastCheckednumber | nullTimestamp of last check
checkUpdate(manual?: boolean) => voidTrigger an update check
downloadAndApplyUpdate() => voidDownload and install the update
setStatus(s: string | null) => voidOverride status message
buildInfoBuildInfoThe build info passed in

OtaUpdateBanner

A ready-made banner component. Accepts theme colors for dark/light mode support.

npx github-ota-init

CLI command that scaffolds the GitHub Actions workflow, build scripts, config plugin, and native setup into your project. For Expo projects it adds the config plugin to app.json; for bare RN projects it patches MainApplication.kt directly.

License

MIT

About

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

react-native-github-ota

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Downloads JS bundles from a GitHub Release (ota-latest tag) and applies them on next app restart — no app-store review needed.

How It Works

  1. A GitHub Action builds your JS bundle on every push to main and uploads it to a GitHub Release tagged ota-latest.
  2. This library compares the current build commit with the remote branch using the GitHub API.
  3. When an update is available, it downloads the bundle to the device and prompts the user to restart.
  4. On next launch, your native bootstrap code loads the downloaded bundle instead of the built-in one.

Installation

npm install react-native-github-ota
# or
yarn add react-native-github-ota

Peer Dependencies

Make sure these are installed in your project:

  • expo-file-system (>= 16)
  • react (>= 18)
  • react-native (>= 0.72)

Quick Start

Run the init command to set everything up automatically:

npx github-ota-init

This will:

  1. Copy the GitHub Actions workflow into .github/workflows/ota-bundle.yml
  2. Add embed-commit and prebuild scripts to your package.json
  3. Add the config plugin to your app.json — native code is applied automatically on expo prebuild
  4. For bare RN projects (no Expo): patches MainApplication.kt directly

After running init, rebuild your native project:

npx expo prebuild --clean

Native Code — Handled Automatically

The library ships an Expo Config Plugin that automatically patches MainApplication.kt during expo prebuild. No manual native code changes needed.

For Expo projects: Just add the plugin to your app.json (the init command does this for you):

{
"expo": {
"plugins": ["react-native-github-ota"]
}
}

For bare React Native projects: The init command patches MainApplication.kt directly. If you need to do it manually, add this:

importjava.io.File// Inside your DefaultReactNativeHost object, add:overridefungetJSBundleFile(): String? {
val otaBundle =File(applicationContext.filesDir, "ota/index.android.bundle")
if (!otaBundle.exists()) returnnull// If the APK was updated after the OTA bundle was downloaded, the OTA is staletry {
val appInfo = applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0)
if (appInfo.lastUpdateTime > otaBundle.lastModified()) {
otaBundle.delete()
File(applicationContext.filesDir, "ota/meta.json").delete()
returnnull
}
} catch (_:Exception) {}
return otaBundle.absolutePath
}

This tells React Native: "if an OTA bundle exists on disk and is newer than the installed APK, load it; otherwise use the built-in one." This prevents stale OTA bundles from overriding a newer rebuild.

Manual Setup

If you prefer to set things up manually:

1. Embed Build Info (required)

Before each build, run the embed-commit script to stamp the current git SHA into your app:

node node_modules/react-native-github-ota/scripts/embed-commit.js

Or add it to your build scripts:

{
"scripts": {
"embed-commit": "node node_modules/react-native-github-ota/scripts/embed-commit.js",
"prebuild": "node node_modules/react-native-github-ota/scripts/embed-commit.js && npx expo prebuild"
}
}

The script creates/updates a constants/buildInfo.ts file in your project root.

2. Add the GitHub Actions Workflow

Copy the workflow template to your project:

mkdir -p .github/workflows
cp node_modules/react-native-github-ota/workflow/ota-bundle.yml .github/workflows/

Or create .github/workflows/ota-bundle.yml manually — see workflow/ota-bundle.yml for the full template.

What the workflow does:

  • Triggers on every push to main
  • Installs dependencies and embeds the commit hash
  • Runs react-native bundle to create the JS bundle
  • Creates an ota-manifest.json with commit metadata
  • Uploads both files to a GitHub Release tagged ota-latest

Note: The workflow uses GITHUB_TOKEN which is automatically provided by GitHub Actions — no extra secrets needed.

Usage

Configure

Call configureOta once at app startup (e.g. in your root layout):

import{configureOta}from"react-native-github-ota";configureOta({owner: "your-github-username",repo: "your-repo-name",branch: "main",// optional, default "main"releaseTag: "ota-latest",// optional, default "ota-latest"bundleFileName: "index.android.bundle",// optional});

Hook

import{useGithubOta}from"react-native-github-ota";import{BUILD_INFO}from"./constants/buildInfo";functionMyComponent(){const{
status,
error,
updateInfo,
isChecking,
isDownloading,
checkUpdate,
downloadAndApplyUpdate,
buildInfo,}=useGithubOta({autoCheckOnMount: true,buildInfo: BUILD_INFO,// Optional: control auto-check with your own settingsshouldAutoCheck: async()=>{// e.g. read from AsyncStoragereturntrue;},});// ...}

Pre-built Banner

import{OtaUpdateBanner}from"react-native-github-ota";<OtaUpdateBannervisible={bannerVisible}status={status}error={error}updateInfo={updateInfo}isDownloading={isDownloading}onDownload={downloadAndApplyUpdate}onClose={()=>setBannerVisible(false)}// Optional theme overrides:colors={{surface: "#1a1a1a",text: "#ffffff",textSecondary: "#a1a1aa",primary: "#3b82f6",danger: "#ef4444",border: "#27272a",}}/>;

API

configureOta(config)

OptionTypeDefaultDescription
ownerstringrequiredGitHub repository owner
repostringrequiredGitHub repository name
branchstring"main"Branch to compare against
releaseTagstring"ota-latest"Release tag containing the OTA bundle
bundleFileNamestring"index.android.bundle"Bundle asset filename in the release
autoSettingsKeystring"@github_ota_settings"AsyncStorage key for settings

useGithubOta(options)

OptionTypeDefaultDescription
buildInfoBuildInforequiredBuild info from embed-commit script
autoCheckOnMountbooleantrueAuto-check for updates on mount
shouldAutoCheck() => boolean | Promise<boolean>Guard for auto-check (e.g. user settings)

Returns:

FieldTypeDescription
statusstring | nullCurrent status message
errorstring | nullError message if any
isAvailablebooleanWhether an update is available
isCheckingbooleanCurrently checking for updates
isDownloadingbooleanCurrently downloading
downloadProgressnumberDownload progress (0-100)
updateInfoUpdateInfo | nullDetails about the available update
lastCheckednumber | nullTimestamp of last check
checkUpdate(manual?: boolean) => voidTrigger an update check
downloadAndApplyUpdate() => voidDownload and install the update
setStatus(s: string | null) => voidOverride status message
buildInfoBuildInfoThe build info passed in

OtaUpdateBanner

A ready-made banner component. Accepts theme colors for dark/light mode support.

npx github-ota-init

CLI command that scaffolds the GitHub Actions workflow, build scripts, config plugin, and native setup into your project. For Expo projects it adds the config plugin to app.json; for bare RN projects it patches MainApplication.kt directly.

License

MIT

About

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

react-native-github-ota

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Downloads JS bundles from a GitHub Release (ota-latest tag) and applies them on next app restart — no app-store review needed.

How It Works

  1. A GitHub Action builds your JS bundle on every push to main and uploads it to a GitHub Release tagged ota-latest.
  2. This library compares the current build commit with the remote branch using the GitHub API.
  3. When an update is available, it downloads the bundle to the device and prompts the user to restart.
  4. On next launch, your native bootstrap code loads the downloaded bundle instead of the built-in one.

Installation

npm install react-native-github-ota
# or
yarn add react-native-github-ota

Peer Dependencies

Make sure these are installed in your project:

  • expo-file-system (>= 16)
  • react (>= 18)
  • react-native (>= 0.72)

Quick Start

Run the init command to set everything up automatically:

npx github-ota-init

This will:

  1. Copy the GitHub Actions workflow into .github/workflows/ota-bundle.yml
  2. Add embed-commit and prebuild scripts to your package.json
  3. Add the config plugin to your app.json — native code is applied automatically on expo prebuild
  4. For bare RN projects (no Expo): patches MainApplication.kt directly

After running init, rebuild your native project:

npx expo prebuild --clean

Native Code — Handled Automatically

The library ships an Expo Config Plugin that automatically patches MainApplication.kt during expo prebuild. No manual native code changes needed.

For Expo projects: Just add the plugin to your app.json (the init command does this for you):

{
"expo": {
"plugins": ["react-native-github-ota"]
}
}

For bare React Native projects: The init command patches MainApplication.kt directly. If you need to do it manually, add this:

importjava.io.File// Inside your DefaultReactNativeHost object, add:overridefungetJSBundleFile(): String? {
val otaBundle =File(applicationContext.filesDir, "ota/index.android.bundle")
if (!otaBundle.exists()) returnnull// If the APK was updated after the OTA bundle was downloaded, the OTA is staletry {
val appInfo = applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0)
if (appInfo.lastUpdateTime > otaBundle.lastModified()) {
otaBundle.delete()
File(applicationContext.filesDir, "ota/meta.json").delete()
returnnull
}
} catch (_:Exception) {}
return otaBundle.absolutePath
}

This tells React Native: "if an OTA bundle exists on disk and is newer than the installed APK, load it; otherwise use the built-in one." This prevents stale OTA bundles from overriding a newer rebuild.

Manual Setup

If you prefer to set things up manually:

1. Embed Build Info (required)

Before each build, run the embed-commit script to stamp the current git SHA into your app:

node node_modules/react-native-github-ota/scripts/embed-commit.js

Or add it to your build scripts:

{
"scripts": {
"embed-commit": "node node_modules/react-native-github-ota/scripts/embed-commit.js",
"prebuild": "node node_modules/react-native-github-ota/scripts/embed-commit.js && npx expo prebuild"
}
}

The script creates/updates a constants/buildInfo.ts file in your project root.

2. Add the GitHub Actions Workflow

Copy the workflow template to your project:

mkdir -p .github/workflows
cp node_modules/react-native-github-ota/workflow/ota-bundle.yml .github/workflows/

Or create .github/workflows/ota-bundle.yml manually — see workflow/ota-bundle.yml for the full template.

What the workflow does:

  • Triggers on every push to main
  • Installs dependencies and embeds the commit hash
  • Runs react-native bundle to create the JS bundle
  • Creates an ota-manifest.json with commit metadata
  • Uploads both files to a GitHub Release tagged ota-latest

Note: The workflow uses GITHUB_TOKEN which is automatically provided by GitHub Actions — no extra secrets needed.

Usage

Configure

Call configureOta once at app startup (e.g. in your root layout):

import{configureOta}from"react-native-github-ota";configureOta({owner: "your-github-username",repo: "your-repo-name",branch: "main",// optional, default "main"releaseTag: "ota-latest",// optional, default "ota-latest"bundleFileName: "index.android.bundle",// optional});

Hook

import{useGithubOta}from"react-native-github-ota";import{BUILD_INFO}from"./constants/buildInfo";functionMyComponent(){const{
status,
error,
updateInfo,
isChecking,
isDownloading,
checkUpdate,
downloadAndApplyUpdate,
buildInfo,}=useGithubOta({autoCheckOnMount: true,buildInfo: BUILD_INFO,// Optional: control auto-check with your own settingsshouldAutoCheck: async()=>{// e.g. read from AsyncStoragereturntrue;},});// ...}

Pre-built Banner

import{OtaUpdateBanner}from"react-native-github-ota";<OtaUpdateBannervisible={bannerVisible}status={status}error={error}updateInfo={updateInfo}isDownloading={isDownloading}onDownload={downloadAndApplyUpdate}onClose={()=>setBannerVisible(false)}// Optional theme overrides:colors={{surface: "#1a1a1a",text: "#ffffff",textSecondary: "#a1a1aa",primary: "#3b82f6",danger: "#ef4444",border: "#27272a",}}/>;

API

configureOta(config)

OptionTypeDefaultDescription
ownerstringrequiredGitHub repository owner
repostringrequiredGitHub repository name
branchstring"main"Branch to compare against
releaseTagstring"ota-latest"Release tag containing the OTA bundle
bundleFileNamestring"index.android.bundle"Bundle asset filename in the release
autoSettingsKeystring"@github_ota_settings"AsyncStorage key for settings

useGithubOta(options)

OptionTypeDefaultDescription
buildInfoBuildInforequiredBuild info from embed-commit script
autoCheckOnMountbooleantrueAuto-check for updates on mount
shouldAutoCheck() => boolean | Promise<boolean>Guard for auto-check (e.g. user settings)

Returns:

FieldTypeDescription
statusstring | nullCurrent status message
errorstring | nullError message if any
isAvailablebooleanWhether an update is available
isCheckingbooleanCurrently checking for updates
isDownloadingbooleanCurrently downloading
downloadProgressnumberDownload progress (0-100)
updateInfoUpdateInfo | nullDetails about the available update
lastCheckednumber | nullTimestamp of last check
checkUpdate(manual?: boolean) => voidTrigger an update check
downloadAndApplyUpdate() => voidDownload and install the update
setStatus(s: string | null) => voidOverride status message
buildInfoBuildInfoThe build info passed in

OtaUpdateBanner

A ready-made banner component. Accepts theme colors for dark/light mode support.

npx github-ota-init

CLI command that scaffolds the GitHub Actions workflow, build scripts, config plugin, and native setup into your project. For Expo projects it adds the config plugin to app.json; for bare RN projects it patches MainApplication.kt directly.

License

MIT

About

GitHub-based OTA (Over-The-Air) updates for React Native / Expo apps.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages