GitHub to APK: how a repository becomes an installable Android app

Quick answer: A GitHub to APK build packages the compiled output of a repository into a signed Android app. The build server does not run your npm, Gradle or Expo build. It needs the finished static files, a package ID, an icon and a signing key, and it returns an APK for sideloading or an AAB for Google Play. A repository that already serves a live HTTPS site can be packaged from that URL, which avoids bundling assets and lets you ship updates without reinstalling.

Most people searching for GitHub to APK want one of two things: an installable version of a web project that lives in a repository, or a way to stop maintaining a separate Android project for something that is already a website.

One rule shapes the workflow. A packaging service converts a finished web build into an Android artifact; it does not compile a source tree. If your repository holds TypeScript, JSX, SCSS or Gradle files, something must turn those into plain HTML, CSS, JavaScript and assets first. That step is your CI pipeline or your laptop.

What part of a repository can be packaged

Only the built output: any folder holding index.html and its assets, whether it sits at the repository root, in dist/, build/, out/, public/ or docs/, or in a branch that contains only the generated site.

Static sites and plain HTML and JavaScript folders: the folder itself. React or Vite: the contents of dist/ after npm run build. Next.js: the static export output in out/. Expo or React Native: the web export, since a React Native Android app is a native project rather than a web build. Capacitor, Cordova, Ionic and Flutter web: the web assets inside the native shell, at android/app/src/main/assets/public, app/src/main/assets/www, www/ or build/web.

What the build server needs from you

Four inputs decide whether the build works: the files, a package ID, an icon and a signing key.

The files, as a repository the builder can read or a ZIP of the built output. The archive need not be tidy: the resolver looks for index.html at the root, then in dist/, build/, out/ or public/, then in the single meaningful top-level folder, then in any folder holding an index.html, then one level deeper. It ignores __MACOSX, dotfiles, Thumbs.db and node_modules.

The package ID is the app's permanent identity, in reverse-domain form such as com.yourcompany.yourapp. It becomes the Android namespace, applicationId and Java package name, so a Java keyword as a segment is rejected: com.private.app fails because private is reserved. The icon must be a real 512 by 512 PNG, because a JPEG renamed to .png fails the Android resource compiler. The app name itself may be written in any script.

Why a repository build differs from a website URL build

A URL build loads your live HTTPS site in a Trusted Web Activity, which is Chrome rendering it full screen. A repository build puts the files inside the APK and loads them from the app's own asset directory.

So nothing has to be hosted, and the app opens with no network. The flip side is updates: a URL build picks up your next deploy immediately, while a bundled build is frozen at the moment you packaged it, so a change means a new build with a higher version code.

Assets must be bundled correctly, which is where a bundled build fails after a clean compile by opening to a white screen. An absolute base path is the usual cause, because /assets/index-abc123.js resolves to the device root inside a file:// origin. Set the base to relative (in Vite, base './') and rebuild. If the project publishes to GitHub Pages, point a URL build at that address instead.

Branch and tag pinning

A build is reproducible only if its input is pinned. Packaging whatever the default branch holds means two APKs built a week apart can behave differently.

Tag the release and take the archive from the tag rather than the branch, or better, attach the built ZIP to a GitHub Release and package that file. A release asset is immutable, so one URL always yields the same bytes, and a tag archive URL contains the tag name, so the same input can be fetched again later.

Keeping versionCode rising so Google Play accepts updates

versionCode is the integer that decides which build is newer. Google Play rejects an upload whose versionCode is not greater than the highest already published for that package: the first can be 1, the next must be 2. versionName is the human-readable string users see, such as 1.0.2.

The failure is quiet. You fix a bug, build, upload, and the console refuses the bundle with a complaint about a duplicate version code. If every build starts from the same configuration, the version code never moves.

Saved projects handle this: a project stores the version code, increments it on each build after the first, and keeps the keystore, so a rebuild needs no re-upload. Only a build signed with the same key can update an installed app.

Typical failures and what they mean

Missing build output. The repository root was uploaded without running the build, so no index.html exists in the archive. The refusal names the folders that were searched. Run the build and upload the output folder, or attach it to a release.

Private repository. A hosted builder cannot clone it without credentials, and you should not paste a long-lived access token into a form you do not control. Download the ZIP from GitHub yourself, or upload the dist artifact your CI produced.

Wrong entry file. Your index.html sits three folders deep in a layout the resolver does not know, for example a nested app folder inside build. Re-zip the contents of the folder that holds index.html so the entry point is at the archive root.

A native project rather than a web build. The archive carries build.gradle, gradle.properties or pubspec.yaml and no index.html. Packaging it would mean running a stranger's build scripts on a machine that holds the service's own secrets, so the console refuses it and names the folder to upload.

Installs, then shows a blank screen. Either the base path problem above or an index.html that is not the built one.

Play refuses the bundle. Nearly always a version code that did not increase, occasionally a changed package name or signing key.

Step-by-step

  1. Produce the built output: Run the web build (npm run build or a static export) and confirm the output folder holds index.html and its assets. Commit it, or attach the ZIP to a tagged release.
  2. Get the archive you will ship: Download the repository ZIP from the branch, tag or release you intend to ship, or zip your output folder so index.html sits at the archive root.
  3. Set the app identity: Choose a reverse-domain package ID, check each segment against Java's reserved words, then supply a 512 by 512 PNG icon and the launcher name.
  4. Pick APK or AAB: Choose APK for sideloading and device testing, or AAB when you upload to Google Play.
  5. Run the build and read the log: Submit the archive and follow the log. Most refusals point at the archive layout rather than the toolchain.
  6. Keep the keystore and version code: Store the project keystore, reuse it for every release of that package, and raise the version code before each Play upload.

Frequently asked questions

Can I convert a GitHub repository straight to an APK without building it?

Not from the source tree. The packager needs compiled static files, so the build step has to happen first: in GitHub Actions, on your laptop, or in whatever CI you already run. Then package the output folder, or package the ZIP you attached to a release.

Which folders inside a repository ZIP are accepted?

index.html at the archive root, in dist/, build/, out/ or public/, in the single meaningful top-level folder, in any folder that holds an index.html, or one level deeper. __MACOSX, dotfiles, desktop.ini, Thumbs.db and node_modules are ignored. The archive limit is 50 MB.

Do I need Android Studio or the Android SDK installed?

No. The toolchain, the signing step and the packaging all run on the build server. You need a browser, your built files, a package ID and an icon. Nothing is installed on your machine.

Can I package a GitHub Pages site instead of a ZIP?

Yes, and it is often the better option. Once the Pages site is live over HTTPS, a URL build loads it directly, so every commit you push is live in the installed app without a rebuild or a reinstall.

What happens with a private repository?

The build server cannot read it without credentials. Download the repository ZIP from GitHub, or upload the dist artifact your CI produced, and the build never needs access to the repository at all.

How do I ship an update without asking users to reinstall?

Only a URL build updates by itself, because it loads your live site. A bundled build is frozen, so an update means a new build with a higher version code. Google Play users then update normally; sideloaded users install the new APK over the old one, which works only if it is signed with the same keystore.

Does an Expo or React Native project work?

The web export does. Run the Expo web export and package the resulting folder. A React Native Android project is itself a native Gradle project, and a native project is not a web build, so it cannot be packaged by this route.

Is the downloaded APK actually signed?

Yes. Builds are signed before you receive them, and each project keeps its own keystore so consecutive builds of one app share an identity. Download and store that keystore, because you need the same key for every future update of that package.

Is there a way to try this without paying?

New accounts receive free credits on signup, which cover a first build. Paid plans add monthly credits and remove the free-tier watermark. An APK build costs 10 credits and an AAB costs 15.