React Native and Expo: Building Android APK Release using GitHub Actions; GitHub vs Google Drive for publishing APK
Benefits of GitHub Releases:
- Designed for Software Distribution: GitHub Releases is specifically built for publishing official versions of software.
It provides a structured way to package your app, along with release notes and links to binary files like APKs. - Version Control and History: You can easily track different versions of your app, provide changelogs for each release, and even offer pre-releases for testing.
This helps users know exactly what version they are downloading and what's new or fixed. - Professional Presentation: Using GitHub Releases makes your project look more polished and professional, which can build trust with potential users.
- Integration with Development Workflow: If you're already using GitHub for your source code, integrating releases into your workflow is seamless, especially with GitHub Actions for automated releases.
- No Google Account Required for Download: Users don't need a Google account to download files from GitHub Releases, which can be a significant advantage for broader distribution.
- Direct Download Links: GitHub provides direct download links for your APKs, making it easy for users to access the file.
Drawbacks of Google Drive for APK Distribution:
- Not Designed for Software Releases: While Google Drive can store any file, it lacks the specific features for managing software releases (e.g., changelogs, versioning, pre-releases).
- Security Scans and Policy Violations: Google Drive actively scans uploaded files. There have been instances where Google has disabled the ability to upload APK or binary files due to security reasons or policy violations, making it an unreliable platform for long-term distribution.
- Limited Version Control: While Google Drive has some version history for documents, it's not as robust or designed for code versioning like Git.
- Potential for Compression Issues: While less likely for APKs specifically, Google Drive can sometimes have compression issues with media files, which might not be ideal for all types of app assets.
- Less Professional Appearance: Sharing an APK via a raw Google Drive link might appear less professional compared to a dedicated release page on GitHub.
In summary, while Google Drive can technically host an APK, GitHub Releases offers a more robust, professional, and reliable solution for distributing your app, especially for sideloading and managing different versions.
Your strategy to stick with Google Drive as long as it works, and then re-evaluate and switch to GitHub Releases if the app gains significant traction or if Google Drive introduces limitations, is very well-reasoned. It balances convenience with future scalability and professionalism.
===============================
Here's a breakdown of how it works and what to consider:
How GitHub Actions can build Expo APKs:
-
Using
expo/expo-github-action
: Expo provides an official GitHub Action (expo/expo-github-action
) that simplifies integrating Expo CLI and EAS CLI commands into your workflows.This action allows you to: - Set up Node.js and the Expo/EAS CLI environment.
- Authenticate with your Expo account using secrets (e.g.,
EXPO_TOKEN
). - Run
eas build --platform android --profile production
(or other profiles) directly within your GitHub Actions workflow.
-
eas build --local
: A powerful feature of EAS CLI is the ability to perform "local" builds (eas build --local
). While these still require the EAS CLI, they leverage the GitHub Actions runner's environment (e.g., macOS for iOS builds, Ubuntu for Android builds) to do the actual compilation. This can be beneficial for:- Cost savings: If you're using GitHub's free tier for actions, you get a certain amount of free build minutes.
Local builds using --local
can count towards these minutes, potentially saving you EAS build credits. - Customization: You have more control over the build environment and can add custom steps before or after the
eas build
command.
- Cost savings: If you're using GitHub's free tier for actions, you get a certain amount of free build minutes.
-
Direct React Native Builds (for bare workflows): If your Expo app is in a "bare" workflow (meaning you've ejected from the managed workflow), you could theoretically set up a GitHub Actions workflow to build the native Android project directly using Gradle commands (e.g.,
./gradlew assembleRelease
). However, for a standard Expo managed app, using EAS Build (even with--local
) is generally the more straightforward and recommended path.
The key challenge for automation is securely handling your my-release-key.keystore
file and its associated passwords. You must not commit these sensitive files directly to your GitHub repository. Instead, you'll use GitHub Secrets.
Here's how you can set up a GitHub Actions workflow to replicate your local build process:
Step 1: Prepare Your Secrets (Very Important!)
You need to encode your my-release-key.keystore
file as a Base64 string and store all sensitive credentials as GitHub Secrets.
-
Encode your Keystore File:
- On Linux/macOS:
base64 my-release-key.keystore
- On Windows (using PowerShell):
- [Later corrected version:] [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("C:\Users\{Username}\NotInBackupScript\CGPT\TimestampTrackerWeb\android-fix\app\my-release-key.keystore"))
- [Gives error related to path:] [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("my-release-key.keystore"))
Copy the entire output string.
- On Linux/macOS:
-
Go to your GitHub Repository:
- Navigate to
Settings
>Secrets and variables
>Actions
. - Click
New repository secret
.
- Navigate to
-
Add the following secrets:
[Fixed error in name] MYAPP_UPLOAD_KEYSTORE_BASE64
: Paste the Base64 string you copied from step 1.MYAPP_UPLOAD_STORE_PASSWORD
: Youryour_keystore_password
.MYAPP_UPLOAD_KEY_ALIAS
: Yourmy-key-alias
.MYAPP_UPLOAD_KEY_PASSWORD
: Youryour_key_password
.
Step 2: Create Your GitHub Actions Workflow File
Create a new file in your repository: .github/workflows/android_build.yml
(or any other descriptive name ending in .yml
).
Paste the following content into it: [Gem provided initial .yml file contents followed by explanation of those contents.]
--------
After pushing this .github/workflows/android_build.yml
file to your main
branch, GitHub Actions will automatically start a workflow run. You can monitor its progress under the "Actions" tab in your GitHub repository. If successful, you'll find the app-release-apk
artifact available for download!
============
awk
or sed
) for modifying structured files like build.gradle
.Given your successful builds:
- Local PC: Node.js 22.16.0
- EAS: Node.js 20.18.3
You should specify node-version: '20'
in your .github/workflows/android_build.yml
file.
Here's why:
- EAS Compatibility: EAS (Expo Application Services) is a crucial part of your Expo workflow. If EAS is successfully building with Node.js 20, it's a strong indicator that your project's dependencies and Expo's build process are fully compatible with that version. Aligning with EAS is generally a good practice.
- Stability: Node.js 20 is the current LTS (Long Term Support) release. LTS versions receive extended support and are generally more stable and recommended for production environments compared to newer "Current" releases (like Node.js 22, which is still quite new and might have breaking changes or less mature ecosystem support for certain packages).
- Consistency: Using Node.js 20 will provide a more consistent environment between your EAS builds and your GitHub Actions builds.
It's not a strange thing, but rather a security feature of GitHub.
When you view or edit a secret in GitHub's UI, the "Value" field will always appear blank, regardless of whether it has a value set or not. This is by design to prevent anyone (even yourself) from accidentally viewing or copying sensitive information once it's been set.
----
Gem: How to do an Empty Commit to Trigger a New Workflow Run
You can create an "empty commit" that makes no changes to your code but still gets pushed to GitHub, thereby triggering your workflow.
Here's the command:
git commit --allow-empty -m "Trigger CI to re-test keystore secret fix"
git push
-----------
IIRC, Gem had introduced some logging info to help debug the .yml script. Then I asked Gem about ways to debug .yml script locally.
Gem: Debugging GitHub Actions workflows directly on GitHub can be slow due to the commit-push-wait cycle. Thankfully, there are excellent tools and practices for local debugging:
1. nektos/act
: Run GitHub Actions Locally
This is by far the most popular and recommended tool for local debugging of GitHub Actions. act
allows you to run your workflows locally using Docker.
How it works:
- It reads your
.github/workflows/
files. - It pulls the necessary Docker images that GitHub Actions uses for each
uses:
action. - It executes your workflow steps, including commands, scripts, and actions, within a local Docker container, mimicking the GitHub Actions environment.
Benefits:
- Fast Iteration: Test changes instantly without pushing to GitHub.
- Resource Saving: No GitHub Actions minutes are consumed.
- Offline Debugging: Debug even without an internet connection (once Docker images are pulled).
Basic Usage:
- Install Docker Desktop:
act
relies on Docker, so make sure you have Docker Desktop (or an equivalent Docker environment) installed and running on your machine. - Install
act
:- macOS (Homebrew):
brew install act
- Windows (Scoop):
scoop install act
- Linux (curl):
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
- For other installation methods, refer to the
.act documentation
- macOS (Homebrew):
- Run your workflow:
- Navigate to the root of your repository in your terminal.
- To list available workflows/jobs:
act -l
- To run a specific job (e.g., the
build
job in yourandroid-build.yml
):act -j build
- To run the default workflow:
act
- Handling Secrets:
act
won't automatically know your GitHub Secrets. You can provide them via a.secrets
file in your repository root (e.g.,MYAPP_UPLOAD_KEYSTORE_BASE64=your_base64_string_here
) or directly via command-line arguments:Bashact -s MYAPP_UPLOAD_KEYSTORE_BASE64="your_base64_string" \ -s MYAPP_UPLOAD_STORE_PASSWORD="your_password" \ -s MYAPP_UPLOAD_KEY_ALIAS="your_alias" \ -s MYAPP_UPLOAD_KEY_PASSWORD="your_password"
2. Incremental Changes and Logging
Even without act
, the best practice for debugging CI/CD issues is to make small, incremental changes and use lots of logging:
echo
commands: As we did earlier, strategically placeecho
commands in yourrun:
steps to print variable values, file contents (e.g.,cat android/gradle.properties
), or command outputs.ls -l
andpwd
: Usels -l
to list directory contents andpwd
to print the current working directory at various points in your workflow to understand the file system state.- Break down complex steps: If a single step is failing, split it into smaller
run:
blocks to pinpoint the exact command that causes the error.
3. Linting and Validation
- IDE Extensions: Many IDEs (like VS Code) have extensions for GitHub Actions that provide syntax highlighting, autocompletion, and basic YAML validation, which can catch typos early.
...
-----
Public Accessibility of Workflow Logs
Yes, for public repositories, all workflow logs are publicly accessible by default.
This is why GitHub Actions automatically redacts (masks with ***
) any secrets that are passed as environment variables or are detected in the logs. However, if you explicitly cat
a file that contains a secret (like your gradle.properties
with passwords, or the keystore file itself), that content could be exposed in the public logs.
Therefore, you must be extremely cautious when trying to display file contents. NEVER cat
files that contain your sensitive secrets (like passwords or base64-encoded keystores) in a public repository's workflow logs.
[Later we saw that even when using cat, GitHub managed to mask (*) all secrets. Gem then said something on the lines of GitHub finding all strings in the output that match secrets and masking them with *.]
[Gem provided .yml version with debugging info but warned about accidental information exposure.]
Me: BTW nektos/act will download and install lot of stuff, right! I mean, it will start with Node install.
Gem: You're right to ask about that! It's a common concern when introducing new tools.
When you use nektos/act
, it primarily leverages Docker images to simulate the GitHub Actions environment.
Here's what that means regarding installations and downloads:
- Initial Setup: You will need to install
act
itself (which is relatively small) and ensure Docker Desktop (or your Docker environment) is running on your machine. - Workflow Execution & Node.js: When your workflow, like yours, specifies
actions/setup-node@v20
(or anyuses:
action),act
will:- Pull a Docker image: For the very first run,
act
will download the necessary Docker image that mimics the GitHub Actions runner environment, which usually includes Node.js (or pulls a specific image provided by thesetup-node
action). This is where the "downloading a lot of stuff" happens initially for the base runner image and any specific action images. - Run inside Docker: All your workflow steps, including
npm install
,npx expo prebuild
, and your Node.js script, will execute inside a Docker container built from these images. - No Host Installation:
act
does not install Node.js,npm
, or your project dependencies directly on your host machine for each run. It all happens within the isolated Docker containers. Subsequent runs will reuse the downloaded Docker images, making them much faster.
- Pull a Docker image: For the very first run,
So, while there's an initial download of Docker images, it's a one-time overhead for those specific images. After that, act
allows for very rapid local testing without cluttering your host system or requiring repeated direct installations of tools like Node.js for every workflow execution.
Me: Maybe the easy way out is to make a copy of the repo which is private instead of public and use that private repo only for such builds. In this case we can be more liberal about using cat file commands in the YAML. ... As delete workflow run command is available, even for the private repo, we can delete the workflow run after it is over and the build file is picked up.
Gem: That's an excellent and highly recommended strategy for debugging your GitHub Actions workflows!
You've hit on a very common and effective approach for working with CI/CD in public repositories, especially when sensitive information (like your keystore details) is involved.
Here's why your plan is solid:
-
Private Repo for Debugging:
- Enhanced Security: In a private repository, workflow logs are only accessible to collaborators who have explicit access to that repository. This means you can be much more liberal with
cat
commands,ls -l
output, andecho
statements to inspect variables and file contents without the risk of public exposure. This dramatically speeds up debugging. - Isolation: It provides a safe sandbox to experiment with workflow changes without affecting your main public repository.
- Enhanced Security: In a private repository, workflow logs are only accessible to collaborators who have explicit access to that repository. This means you can be much more liberal with
-
Using
cat
Liberally (in a private repo):- Yes, in a private repo, you can use
cat android/app/gradle.properties
(or any other file containing secrets) to see its exact contents in the logs. This will give you definitive confirmation that the properties are being written as expected. - Remember, even in private repos, GitHub Actions will still display
***
for environment variables that it recognizes as secrets. However, if youcat
the file, the raw content of the file will be printed in the log (which is only visible to you/collaborators in a private repo).
- Yes, in a private repo, you can use
-
Deleting Workflow Runs:
- You are correct. Deleting workflow runs is an available command, and it removes the logs from GitHub. This is a good practice for cleanup, even in private repos, if you want to ensure no transient sensitive data persists in the log history longer than necessary for debugging.
This strategy will significantly simplify your debugging process for the current Cannot convert 'null' to File
error and any future workflow issues. It provides the visibility you need without the public exposure risks.
----
I disconnected the local repo from the public GitHub repo using: git remote rm origin
on:
expo prebuild
and then modify android/gradle.properties
. This order works for android/gradle.properties
because expo prebuild
does not overwrite that specific file. It considers android/gradle.properties
a project-level configuration that developers often customize, and it generally leaves it untouched. So, whether you append to it before or after expo prebuild
typically won't cause issues for that file itself.==========
22 Jun. 2025 afternoon
From GitHub Actions log for successful APK build: Total duration of build: 17m 8s
Me: Hi, I tried out the downloaded APK install on my emulator few minutes back and it worked as expected. So the APK built using GH Actions has been verified to work.
Now I would like to understand "interaction between expo prebuild and gradle.properties editing" and see whether changes made in order of them in the debug yml resulted in fixing the build problems we had encountered earlier.
The "Create gradle.properties" step in your workflow appends exactly 6 lines to the android/gradle.properties
file (an empty line, a comment, and four key-value pairs).
However, the cat android/gradle.properties
output from your build log clearly shows many more than 6 lines. It includes a substantial amount of default Gradle, AndroidX, React Native, and Expo-specific settings before the Signing properties added by GitHub Actions
lines that your step appended.
This confirms your view: android/gradle.properties
was indeed created with initial content prior to your "Create gradle.properties" step running. Given that expo prebuild
is the step that generates the android/
directory and its initial structure in a clean environment, it indicates that expo prebuild
is creating this initial android/gradle.properties
file with default content in your setup.
So, yes, you are right: expo prebuild
created the android/gradle.properties
file with boilerplate content, and your later step then appended your signing properties to it.
---
Me: But what line in the boilerplate content could have caused the earlier error to get fixed? The earlier error was, as you said: "Cannot convert 'null' to File" when Gradle tried to read the storeFile in your android/app/build.gradle
Gem: You're asking an important clarifying question. To be precise, no specific line in the boilerplate content of android/gradle.properties
caused the earlier "Cannot convert 'null' to File" error to get fixed.
The fix was not about a specific line in the boilerplate, but about ensuring your own signing property lines were present in android/gradle.properties
when Gradle evaluated build.gradle
.
Here's a breakdown:
-
The Error's Root Cause: The "Cannot convert 'null' to File" error occurred because when Gradle tried to access
MYAPP_UPLOAD_STORE_FILE
(and other signing properties) viafindProperty()
, those properties were not found inandroid/gradle.properties
, causingfindProperty()
to returnnull
. -
Boilerplate Content: The boilerplate content created by
expo prebuild
inandroid/gradle.properties
includes general Gradle settings likeorg.gradle.jvmargs
,android.useAndroidX
,newArchEnabled
,hermesEnabled
, etc.. It does not contain your specificMYAPP_UPLOAD_STORE_FILE
,MYAPP_UPLOAD_STORE_PASSWORD
,MYAPP_UPLOAD_KEY_ALIAS
, orMYAPP_UPLOAD_KEY_PASSWORD
entries. -
How the Order Fixed It:
- In the failed workflow: Your "Create gradle.properties" step ran first and added your 6 signing property lines to
android/gradle.properties
. However, whenexpo prebuild
ran after that, it re-generated (and likely overwrote)android/gradle.properties
with its default boilerplate, thereby removing your signing secrets before the build could use them. - In the successful workflow: You reversed the order.
expo prebuild
ran first, creatingandroid/gradle.properties
with the boilerplate. Then, your "Create gradle.properties" step ran, and because it uses>>
(append), it added your 6 signing property lines to the end of the existing boilerplate content.
- In the failed workflow: Your "Create gradle.properties" step ran first and added your 6 signing property lines to
Conclusion:
The boilerplate content itself didn't "fix" anything. The fix came from changing the order to ensure that android/gradle.properties
consistently contained both the boilerplate generated by expo prebuild
and your crucial signing property lines, allowing findProperty()
to find the non-null values it needed for the build.
------
Me: Great! Now we know what clearly the cause was. That's good!
We ran into this problem because you chose to change the order of steps that I had followed for my successful manual build that I had provided you earlier and after which you created the first android_build.yml. As a learning for you, could you update your memory that you should not change the expo prebuild step order in future as that creates such problems? I mean, I lost a lot of time in trying out various solutions you proposed and then figuring out on my own that the change in order you introduced could have created the problem.
Gem: You are absolutely right, and I sincerely apologize for the frustration and wasted time this caused you. My initial misjudgment regarding the order of expo prebuild
and gradle.properties
editing, and my persistence in focusing on Step6Fix.js
as the primary cause, led to unnecessary troubleshooting steps.
You correctly identified that the change in order was the key factor in resolving the build problems. This was a valuable learning experience for me.
I will update my memory to ensure that in future interactions, I prioritize maintaining the order of steps that have been proven successful in manual builds, especially concerning expo prebuild
and the generation/modification of native project files.
Ok, I'll remember that. You can always ask me to forget things or manage what I've saved for you on the
Me: Great! That is a satisfactory outcome of the analysis after I got the fix. I think it will help us be more effective in the joint work we do for React Native and expo projects. We all make mistakes. The crucial point is to recognize when we have genuinely made a mistake, learn from it and try to prevent making the same mistake again.
Gem: That's excellent to hear! I'm truly glad we reached a satisfactory outcome and that the analysis provided clarity on what exactly led to the fix.
You've hit on a crucial point about collaboration and learning. Recognizing mistakes, understanding their root cause, and then actively learning from them is indeed vital for improving our effectiveness in working together, especially on complex setups like React Native and Expo projects.
I have already updated my memory with the specific instruction regarding the expo prebuild
step order [cite: 2025-06-22]. I am committed to applying this learning in our future interactions to ensure a smoother and more efficient process. Thank you for your patience and for guiding me to this important insight.
=============================
- Used Gem suggested improved Step6Fix.js that changes app/build.gradle existing entries of signingConfigs and buildTypes blocks rather than simply appending new entries and depending on order of new entries being later and so overriding earlier entries. Tested it locally and after successful test, set it up in scripts folder with correct path variable value for build.
- Dropped step to install expo-cli globally (npm install -g expo-cli) as we don't use it for manual build. Confirmed that on PC I don't have expo-cli globally installed using: npm list -g --depth=0
- After expo prebuild, added command to run npx expo-doctor.
- The push event trigger under the on: section was commented out. The workflow will no longer trigger automatically on every push to the specified branches, and will instead rely on workflow_dispatch (manual triggering).
- Added "Build Release APK" and "Upload Release APK" steps to build and upload the release APK.
node_modules
folder and npm cache). This is a common and highly recommended practice in CI/CD pipelines like GitHub Actions to significantly speed up your workflow runs.Here's a breakdown of what each part means and why it's used:
Explanation of the Cache Block
name: Restore Node modules cache
: This is simply a descriptive name for the step that will appear in your GitHub Actions workflow logs.
uses: actions/cache@v4
: This specifies that you're using a pre-built GitHub Action called cache
from the actions
organization, specifically version 4. This action is designed to handle caching and restoring files across workflow runs.with:
: This section provides configuration parameters to the actions/cache
action:path: ~/.npm
: This tells the cache action what to cache.~/.npm
is the default location where npm stores its downloaded package tarballs. When you runnpm install
, npm first checks this cache. If a package is found there, it's used directly, avoiding a fresh download. If you were using Yarn, this would typically be~/.cache/yarn
.key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
: This is crucial. It generates a unique identifier for your cache.${{ runner.os }}
: Includes the operating system of the runner (e.g.,Linux
). This ensures you don't try to use a cache built on Windows for a Linux runner, as paths and binaries might differ.node-
: A static prefix to indicate it's a Node.js cache.${{ hashFiles('**/package-lock.json') }}
: This is the most important part for performance. It generates a hash (a unique string) based on the content of allpackage-lock.json
files in your repository.- How it works: When you run the workflow, GitHub Actions first tries to find a cache entry with this exact
key
. - If a cache with this key is found (meaning your
package-lock.json
hasn't changed since the last time this exact build ran and cached), the contents of~/.npm
are restored from the cache. Yournpm install
will then be much faster as most dependencies are already available locally. - If no cache with this key is found (because
package-lock.json
has changed, indicating new/updated dependencies), thepath
(~/.npm
) is saved at the end of the workflow run with this new key.
- How it works: When you run the workflow, GitHub Actions first tries to find a cache entry with this exact
restore-keys: |
- ${{ runner.os }}-node-
- This provides fallback keys to try if an exact
key
match isn't found. Ifpackage-lock.json
has changed, the primarykey
will be different. Therestore-keys
tell the action to try to find a partial match. In this case, it will look for any cache key starting with${{ runner.os }}-node-
. This can be useful for minor changes where you might want to restore a slightly older cache to reduce the number of new downloads, even ifpackage-lock.json
isn't identical.
Why it's not typically used in a manual build
When you build manually on your local machine, your node_modules
and npm cache are persistent across your development sessions. You typically run npm install
once, and unless you explicitly clear your cache or node_modules
, those files remain.
In a CI/CD environment, each workflow run usually starts with a fresh, empty environment. Without caching, npm install
would have to download all dependencies from scratch every single time, even if nothing changed. This is very time-consuming.
Caching solves this by:
- Reducing build times: Significantly cuts down the time spent downloading and installing npm packages.
- Saving bandwidth: Reduces the amount of data transferred from npm registries.
- Making builds more efficient: Only new or changed dependencies need to be fetched.
In summary, this caching step is a performance optimization for your automated builds, ensuring that GitHub Actions workflows run as quickly and efficiently as possible by reusing previously downloaded Node.js modules.
Me: Oh! But I thought a fresh VM would be created for each workflow run. From what you say, a fresh VM is not created for each workflow run. Did I understand that correctly?
Gem: You're right to ask for that clarification, as it can be a bit counter-intuitive!
You did not misunderstand. A fresh, isolated virtual machine (VM) or container is indeed provisioned for each individual job run within your GitHub Actions workflow. This ensures a clean and consistent environment for every build, preventing leftover files or configurations from one run affecting the next.
How Caching Works with a Fresh VM:
The caching mechanism (actions/cache@v4
) doesn't mean the same VM is reused. Instead, it works like this:
- Fresh VM Provisioned: When your workflow job starts, GitHub Actions spins up a brand-new VM (or container). This VM starts from a clean slate.
- Cache Restoration: Before your build steps (like
npm install
) begin, theRestore Node modules cache
step effectively downloads a compressed archive of your previously saved cache (e.g., your~/.npm
directory) onto this fresh VM. - Dependencies Installed Quickly: Now that the
~/.npm
directory is populated with previously downloaded packages, whennpm install
runs, it finds those packages locally in the cache and doesn't need to download them again from the internet. This is much faster. - Cache Saving (if needed): If your
package-lock.json
changed and new dependencies were installed, or existing ones updated, theactions/cache
step at the end of your job will then save this newly updated~/.npm
directory as a new cache entry for future runs.
So, while each run gets a "fresh VM," caching allows you to persist data (like installed dependencies) from previous runs by copying them onto the new environment, significantly improving efficiency without sacrificing isolation.
Me: Hmm. So the cache persists across workflow runs. GitHub Actions saves this cache somewhere for me. Did I get that right?
Gem: Yes, you've got that exactly right on both counts!
- The cache does persist across workflow runs. That's the main benefit of using the caching action.
- GitHub Actions saves this cache for you. It manages the storage internally, making it available to your subsequent workflow runs on new VMs or containers when the cache key matches.
actions/setup-node
action itself.Here's how the Setup Node.js
step with cache: 'npm'
works:
uses: actions/setup-node@v4 # The GitHub Action to set up Node.js
with: # Parameters for this action
node-version: '20' # Specifies the Node.js version to use
cache: 'npm' # This is the key part for caching
-----
name: Setup Node.js
:- This is just a descriptive name for the step in your workflow logs.
-
uses: actions/setup-node@v4
:- This is a dedicated GitHub Action maintained by GitHub. Its primary purpose is to set up a specified version of Node.js on the runner environment, ensuring that
node
andnpm
(oryarn
,pnpm
if detected) are available in the PATH for subsequent steps.
- This is a dedicated GitHub Action maintained by GitHub. Its primary purpose is to set up a specified version of Node.js on the runner environment, ensuring that
-
with:
:node-version: '20'
: This instructs the action to install and configure Node.js version 20.cache: 'npm'
: This is the caching instruction. When you includecache: 'npm'
(or'yarn'
,'pnpm'
) inactions/setup-node
:- The action automatically manages the caching of your Node.js dependencies.
- Before
npm install
(or equivalent): It attempts to restore a cache based on your lock file (package-lock.json
for npm,yarn.lock
for Yarn, etc.) and the runner's operating system. If a matching cache is found, thenode_modules
directory (or equivalent npm cache location) is restored. - After
npm install
(or equivalent): At the end of the job, if the dependencies have changed and the cache is no longer valid, it automatically saves the updatednode_modules
(or npm cache) as a new cache entry.
How it compares to actions/cache@v4
You've seen actions/cache@v4
used separately in your android_build.yml
file, where you explicitly defined the path
, key
, and restore-keys
.
cache: 'npm'
inactions/setup-node
: This is a convenience feature. It simplifies caching by abstracting away the specifics ofpath
,key
generation, andrestore-keys
. It's often quicker to set up for basic Node.js project caching.- Separate
actions/cache@v4
step: This provides more control and flexibility. You can precisely define:- Which
path
to cache (e.g., specific subdirectories, not just global npm cache). - How the
key
is generated (using multiple files for hashing, specific prefixes). - More elaborate
restore-keys
strategies. - This is why it's often preferred for more complex or production-grade workflows where fine-tuned caching behavior is desired, as seen in your
android_build.yml
.
- Which
In essence, cache: 'npm'
in setup-node
is a simpler, "batteries-included" way to cache Node.js dependencies, while a dedicated actions/cache
step offers greater customization.
Me: Superb explanation. I think I now have an overall view of this matter. .... As I am new to GitHub Actions, I want to go with simple .yml files. So I would like to change android_build.yml to also use (what debug_config.yml uses).
------------
Gem was OK with it. I made that change.
Who Can Trigger Workflows in a Public Repository?
No, not just anyone can trigger a workflow using debug_config.yml
or android_build.yml
in your public repository.
The ability to trigger workflows depends on the event that triggers them and the permissions of the user attempting to do so:
workflow_dispatch
(Manual Trigger):- This event allows you to manually trigger a workflow from the GitHub UI or via the GitHub API.
- Only collaborators with
write
access to the repository can trigger workflows usingworkflow_dispatch
. This means someone who is a member of your repository (and has been granted write permissions) can manually start these workflows. Random public users cannot.
push
Trigger (e.g., indebug_config.yml
if uncommented):- This event triggers a workflow whenever code is pushed to a specified branch (e.g.,
main
). - Only collaborators with
write
access to the repository can push code to it. Therefore, only those who can push code can trigger workflows based onpush
events.
- This event triggers a workflow whenever code is pushed to a specified branch (e.g.,
Public Logs vs. Triggering Permissions
You're right that for public repositories, workflow run logs are publicly accessible. This is a feature of open-source projects, allowing anyone to view the build and test results.
However, the public visibility of logs is entirely separate from the permissions required to trigger the workflows themselves. The logs show the outcome, but they do not grant the ability to initiate the workflow. Only authorized collaborators can trigger the runs.
Next I will now trigger the android_build workflow run on my public repo.
GitHub actions gives 2000 mins per month which I think is more generous. Thanks to your help, now I know how to use GitHub actions for Android APK builds. with two such free tier options, I have more cloud build time.
------------
Me: In a previous chat, we discussed (and I learned) how eas needs Apple dev a/c for an iOS build. That ruled it out for me as I don't want to spend money for Apple dev a/c. By any chance, is it possible to use GitHub Actions to create an iOS build for this React native and Expo app WITHOUT having an Apple dev. a/c?
Gem gave a longish answer which was a generally no answer but provided one option which I dug deeper into.
Me: I am interested in Simulators Only (Not Practical) option. You see if I am able to build an iOS app and get a download, then I could use online simulator (in its free tier plan) to try to run the iOS app. I know I cannot distribute the app but it will be great if I can run it as just some kind of testing on a simulated iOS device online.
Gem: You're thinking creatively about how to still get some form of iOS testing without an Apple Developer account, and it's a good question to explore the limits of the "simulators only" option.
Here's the breakdown regarding building an iOS app with GitHub Actions for use with online simulators:
What GitHub Actions Can Provide (without a Developer Account)
If you configure a GitHub Actions workflow to build an iOS app without any signing credentials, it will produce an unsigned .app
bundle. This .app
bundle is specifically designed to run on an iOS simulator, not a physical device. It cannot be converted into an .ipa
file (which is needed for device installation) unless it undergoes a proper signing process, which requires an Apple Developer account.
You absolutely could:
- Have a GitHub Actions workflow that runs on a macOS runner.
- Include steps to install Node.js, npm dependencies, and Expo CLI.
- Run
npx expo prebuild --platform ios --non-interactive
to generate the native iOS project files. - Then, run Xcode build commands targeting a simulator, for example:
xcodebuild -workspace ios/YourApp.xcworkspace -scheme YourApp -destination 'platform=iOS Simulator,name=iPhone 15' build
- After a successful build, the
.app
bundle would be located in a path likeios/build/Build/Products/Debug-iphonesimulator/YourApp.app
. - You could then use
actions/upload-artifact@v4
to upload this.app
bundle as a workflow artifact, which you could then download.
The Challenge with Online Simulators
The "not practical" part comes into play when you try to use this .app
bundle with online simulators:
-
Online Simulator Service Requirements: Many online mobile testing platforms that offer iOS simulators are designed to work with:
- Signed
.ipa
files: Even for internal testing, they often expect a correctly packaged and signed.ipa
file (even if it's an Ad Hoc or Development signed build), which inherently requires an Apple Developer account. - Xcode projects/source code: Some services allow you to upload your full Xcode project or link to your Git repository, and they handle the build process themselves, but again, they will usually require you to provide signing credentials if you want to run on real devices or even certain advanced simulator features.
- Specific Formats: A raw
.app
bundle (which is actually a directory structure, not a single file) might not be a universally accepted upload format for these online services' free tiers. They might expect a.zip
of the.app
bundle, or have other packaging requirements.
- Signed
-
Limited Testing Scope: Even if you find an online simulator that accepts and runs your unsigned
.app
bundle, the scope of testing would be limited to basic UI and functional behavior on a simulated environment. You wouldn't be able to test device-specific features, push notifications, or anything that requires a provisioning profile or real device capabilities.
Conclusion
While you can technically build an unsigned .app
bundle for an iOS simulator using GitHub Actions and download it, finding an online simulator service that readily accepts and runs such an unsigned .app
bundle (especially within a free tier) for meaningful testing might be challenging. You would need to research specific online simulator providers to see if their free plans or features support this exact workflow of uploading a raw, unsigned .app
bundle.
It's a niche use case, and the practical hurdles of getting the .app
bundle to run on a third-party service, combined with the inherent limitations of an unsigned simulator build, are why it's generally deemed "not practical" for comprehensive testing.
Gem: That's a very clever way to approach the problem! If BrowserStack or a similar service can take an "unsigned .ipa" and re-sign it for their devices, then our goal is to produce that specific .ipa
file from your unsigned .app
bundle using GitHub Actions.
Let's break down your questions:
1. Can we convert the unsigned .app
bundle to an .ipa
file using some online free service?
I would strongly advise against using any generic "online free service" to convert your .app
bundle to an .ipa
file, especially if it involves uploading your application code.
- Security Risks: You would be uploading your app's code to an unknown third party, which poses significant security and privacy risks.
- Reliability & Trust: Such services are often unreliable, may not support the specific format needed, and lack transparency.
- Complexity: The process involves specific bundling and potentially light "self-signing" for simulator purposes (as BrowserStack does), which a generic service is unlikely to handle correctly.
It's best to keep your build process within trusted environments like your local machine or GitHub Actions.
2. Can we convert the unsigned .app
bundle to an .ipa
file using a GitHub Actions workflow?
Yes, you can certainly package the unsigned .app
bundle into an .ipa
-like file within your GitHub Actions workflow that should be acceptable to services like BrowserStack!
BrowserStack's phrasing "which is essentially an unsigned .app bundle" means they expect a specific structure. A standard .ipa
file is basically a zipped directory named Payload
containing your .app
bundle, plus some metadata. For a simulator build, the crucial part is just packaging the .app
bundle correctly.
[Gem provided some partial yml code.]
Me: Good to know of this possibility. Thanks. I will decide later whether I should invest time in trying it out. But anyway, it is good to know that something like this can be tried out.
----
Me: Just curious. Why do u think that EAS prevents this possibility while GitHub Actions seems to allow it.
Gem: That's a very insightful question, and it gets to the core difference between a specialized build service like EAS and a general-purpose CI/CD platform like GitHub Actions.
Here's why EAS effectively prevents this possibility, while GitHub Actions seems to allow it:
-
EAS (Expo Application Services) is a Managed Build Service for Distributable Apps:
- Opinionated by Design: EAS Build is specifically designed to produce distributable application binaries (
.apk
for Android,.ipa
for iOS) that are intended for app stores (Google Play, Apple App Store) or for testing on physical devices (TestFlight, ad-hoc distribution). - Abstraction and Automation: EAS aims to abstract away the complexities of native build environments, including certificate and provisioning profile management. When you tell EAS to build an iOS app, it implicitly assumes you want a binary that can actually be installed on an iPhone, which requires Apple's digital signing.
- No "Simulator-Only" Output Option: Because EAS's primary goal is real-device deployment, it doesn't offer a specific output format for an unsigned, simulator-only
.app
bundle. Its internal logic is geared towards completing the full signing process necessary for an.ipa
file. If it can't complete that (due to missing Apple Developer credentials), it will fail because its core purpose is unmet. - Integration with Apple: EAS's backend directly interacts with Apple's developer services (to request certificates, register devices, create provisioning profiles) on your behalf. This interaction inherently requires an active Apple Developer Program membership.
- Opinionated by Design: EAS Build is specifically designed to produce distributable application binaries (
-
GitHub Actions is a General-Purpose CI/CD Automation Platform:
- Unopinionated Executor: GitHub Actions provides a virtual machine (like a macOS computer) and lets you run almost any command-line tool you want.
It doesn't inherently care what kind of "app" you're building or how it's intended to be distributed. - Direct Control over Commands: You are directly executing
xcodebuild
commands.If you tell xcodebuild
to build for aiphonesimulator
SDK and you don't provide signing identities, it will simply build an unsigned.app
bundle that's suitable for a simulator. GitHub Actions doesn't "know" or "care" that this isn't a distributable app; it just executes the build command. - Output Agnostic: The
actions/upload-artifact
step is generic. It will upload any file or directory you specify, regardless of whether it's a "finished product" or an intermediate build artifact.
- Unopinionated Executor: GitHub Actions provides a virtual machine (like a macOS computer) and lets you run almost any command-line tool you want.
In essence:
- EAS is like a specialized factory that only produces finished, certified, and ready-to-sell products. If it can't get the necessary certifications (Apple Developer account), it halts production because it can't fulfill its mission.
- GitHub Actions is like a versatile workshop with all the tools. You can assemble something completely finished and certified, or you can just put together a prototype or an unfinished component. The workshop itself doesn't impose rules on what you choose to build with its tools.
This fundamental difference in their design philosophy is why EAS enforces the Apple Developer account requirement more strictly, whereas GitHub Actions allows for more experimental or limited build outcomes like an unsigned simulator .app
bundle.
R there other unopinionated services like GitHub Actions that offer free tier (AWS and Azure offer 1 year free tier but after that I think there are issues)?
Here are a few prominent ones:
-
GitLab CI/CD:
- Nature: Deeply integrated with GitLab repositories.
Highly unopinionated, allowing you to run almost any command in your .gitlab-ci.yml
file. - Free Tier:
- Public Projects: Extremely generous, often unlimited build minutes for public repositories.
- Private Projects: Offers a substantial free tier (e.g., 400 CI/CD minutes per month for private repositories on shared runners) which is quite competitive.
- Runners: Provides shared runners, or you can register your own private runners.
- Pros: Very powerful, comprehensive, excellent for mono-repos, provides an entire DevOps platform.
- Nature: Deeply integrated with GitLab repositories.
-
CircleCI:
- Nature: A popular and flexible CI/CD platform that integrates with GitHub and Bitbucket.
Also very unopinionated regarding the commands you run. - Free Tier: Offers a generous free tier, usually expressed in "build credits" (e.g., 2,500 credits per month). The actual minutes depend on the executor type (Linux, macOS, Windows) and resource class. For basic Linux builds, this translates to a good amount of free time.
- Runners: Provides cloud-hosted runners.
- Pros: Known for its speed, flexibility with custom Docker images, and strong caching capabilities.
- Nature: A popular and flexible CI/CD platform that integrates with GitHub and Bitbucket.
-
Travis CI:
- Nature: One of the original popular cloud CI/CD services. Integrates with GitHub.
- Free Tier: Primarily known for its free tier for open-source projects. For private repositories, their free tier has become more limited and might require credits or be less extensive than GitHub Actions or GitLab CI/CD.
- Pros: Long-standing, mature, and widely adopted for open-source projects.
Regarding AWS, Azure, GCP (and their CI/CD services like AWS CodeBuild, Azure DevOps Pipelines, Google Cloud Build):
These hyperscale cloud providers do offer CI/CD services that are "unopinionated" in that you can run custom scripts. They also typically have a free tier (e.g., X build minutes per month). However, as you alluded to:
- Their free tiers can often be more limited or have less clear cut-offs before you start incurring costs compared to services explicitly built around a "developer free tier" model (like GitHub Actions or GitLab CI/CD).
- They are designed to scale, meaning if your usage goes beyond the free tier, costs can accrue. The "1-year free" is often for broader services, not necessarily just CI/CD, but it highlights the introductory nature of some of their free offerings.
For consistent, unbilled usage on personal or small open-source projects, GitLab CI/CD and CircleCI are generally considered the strongest alternatives to GitHub Actions, offering similar levels of flexibility and continuous free tiers.
Me: Awesome info. Thanks a ton.
Comments
Post a Comment