<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Aleksandr Ilinskiy</title>
    <description>The latest articles on Forem by Aleksandr Ilinskiy (@aleksandr_ilinskiy).</description>
    <link>https://forem.com/aleksandr_ilinskiy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3780142%2F2e210ed9-004a-4cd7-a9c3-95ddc0d38a94.png</url>
      <title>Forem: Aleksandr Ilinskiy</title>
      <link>https://forem.com/aleksandr_ilinskiy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/aleksandr_ilinskiy"/>
    <language>en</language>
    <item>
      <title>How to Set Up Android CI/CD with GitHub Actions — Firebase Distribution &amp; Play Store</title>
      <dc:creator>Aleksandr Ilinskiy</dc:creator>
      <pubDate>Thu, 19 Feb 2026 21:35:24 +0000</pubDate>
      <link>https://forem.com/aleksandr_ilinskiy/how-to-set-up-android-cicd-with-github-actions-firebase-distribution-play-store-30nk</link>
      <guid>https://forem.com/aleksandr_ilinskiy/how-to-set-up-android-cicd-with-github-actions-firebase-distribution-play-store-30nk</guid>
      <description>&lt;p&gt;Setting up CI/CD for Android apps on GitHub Actions is straightforward once you know the gotchas. This guide covers everything: building signed APKs/AABs, caching Gradle, deploying to Firebase Distribution for testers, and publishing to Play Store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An Android project with Gradle (Groovy or Kotlin DSL)&lt;/li&gt;
&lt;li&gt;A GitHub repository&lt;/li&gt;
&lt;li&gt;For Firebase: a Firebase project with your app added&lt;/li&gt;
&lt;li&gt;For Play Store: a Google Play Console account with your app set up&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Make Gradlew Executable
&lt;/h2&gt;

&lt;p&gt;This trips up almost everyone on their first CI run. Your &lt;code&gt;gradlew&lt;/code&gt; file might not be executable in Git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git update-index &lt;span class="nt"&gt;--chmod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;+x gradlew
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Make gradlew executable"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add this step to your workflow (we'll include it below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a Signing Keystore
&lt;/h2&gt;

&lt;p&gt;If you don't have one yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;keytool &lt;span class="nt"&gt;-genkeypair&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keystore&lt;/span&gt; release.jks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keyalg&lt;/span&gt; RSA &lt;span class="nt"&gt;-keysize&lt;/span&gt; 2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-validity&lt;/span&gt; 10000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-alias&lt;/span&gt; release &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-storepass&lt;/span&gt; YOUR_STORE_PASSWORD &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keypass&lt;/span&gt; YOUR_KEY_PASSWORD &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-dname&lt;/span&gt; &lt;span class="s2"&gt;"CN=Your Name, O=Your Org"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Never commit the keystore to Git.&lt;/strong&gt; Instead, base64-encode it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; release.jks | pbcopy   &lt;span class="c"&gt;# macOS&lt;/span&gt;
&lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; 0 release.jks          &lt;span class="c"&gt;# Linux&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Add GitHub Secrets
&lt;/h2&gt;

&lt;p&gt;Go to your repo → &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt; and create:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KEYSTORE_BASE64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Base64-encoded .jks file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KEYSTORE_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your keystore password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KEY_ALIAS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your key alias (e.g. "release")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KEY_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your key password&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For &lt;strong&gt;Firebase Distribution&lt;/strong&gt;, also add:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FIREBASE_APP_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Firebase Console → Project Settings → Your Android app ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FIREBASE_SERVICE_ACCOUNT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON content of a Firebase service account key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For &lt;strong&gt;Play Store&lt;/strong&gt;, add:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PLAY_SERVICE_ACCOUNT_JSON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google Play Console → API access → Service account JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 4: The Workflow — Firebase Distribution
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/android-firebase.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Android — Build &amp;amp; Deploy to Firebase&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workflow }}-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build &amp;amp; Upload to Firebase&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up JDK &lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temurin'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache Gradle&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;~/.gradle/caches&lt;/span&gt;
            &lt;span class="s"&gt;~/.gradle/wrapper&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ runner.os }}-gradle-&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Make gradlew executable&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chmod +x ./gradlew&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Decode Keystore&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d &amp;gt; app/release.jks&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Release APK&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew assembleRelease&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;KEYSTORE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KEYSTORE_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;KEY_ALIAS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KEY_ALIAS }}&lt;/span&gt;
          &lt;span class="na"&gt;KEY_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KEY_PASSWORD }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload to Firebase Distribution&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wzieba/Firebase-Distribution-Github-Action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FIREBASE_APP_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;serviceCredentialsFileContent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FIREBASE_SERVICE_ACCOUNT }}&lt;/span&gt;
          &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testers&lt;/span&gt;
          &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app/build/outputs/apk/release/app-release.apk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: The Workflow — Play Store
&lt;/h2&gt;

&lt;p&gt;For Play Store deployment, the workflow is similar but uses AAB (Android App Bundle) instead of APK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Android — Build &amp;amp; Publish to Play Store&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*'&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workflow }}-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build &amp;amp; Publish to Play Store&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up JDK &lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temurin'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache Gradle&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;~/.gradle/caches&lt;/span&gt;
            &lt;span class="s"&gt;~/.gradle/wrapper&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ runner.os }}-gradle-&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Make gradlew executable&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chmod +x ./gradlew&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Decode Keystore&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d &amp;gt; app/release.jks&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Release AAB&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew bundleRelease&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;KEYSTORE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KEYSTORE_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;KEY_ALIAS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KEY_ALIAS }}&lt;/span&gt;
          &lt;span class="na"&gt;KEY_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KEY_PASSWORD }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload to Play Store&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;r0adkll/upload-google-play@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;serviceAccountJsonPlainText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}&lt;/span&gt;
          &lt;span class="na"&gt;packageName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.yourcompany.yourapp&lt;/span&gt;
          &lt;span class="na"&gt;releaseFiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app/build/outputs/bundle/release/app-release.aab&lt;/span&gt;
          &lt;span class="na"&gt;track&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Replace &lt;code&gt;com.yourcompany.yourapp&lt;/code&gt; with your actual package name. The &lt;code&gt;track&lt;/code&gt; can be &lt;code&gt;internal&lt;/code&gt;, &lt;code&gt;alpha&lt;/code&gt;, &lt;code&gt;beta&lt;/code&gt;, or &lt;code&gt;production&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Configure build.gradle for CI Signing
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;app/build.gradle.kts&lt;/code&gt; needs to read signing config from environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;android&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;signingConfigs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"release"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;storeFile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"release.jks"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;storePassword&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"KEYSTORE_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;keyAlias&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"KEY_ALIAS"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;keyPassword&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"KEY_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;buildTypes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;release&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;signingConfig&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signingConfigs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"release"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;isMinifyEnabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
            &lt;span class="nf"&gt;proguardFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;getDefaultProguardFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"proguard-android-optimize.txt"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s"&gt;"proguard-rules.pro"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Issues
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Permission denied: gradlew"&lt;/strong&gt; — Either run &lt;code&gt;chmod +x ./gradlew&lt;/code&gt; in your workflow or fix it in Git with &lt;code&gt;git update-index --chmod=+x gradlew&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"JAVA_HOME is not set"&lt;/strong&gt; — Make sure the &lt;code&gt;setup-java&lt;/code&gt; step comes before any Gradle commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build takes too long&lt;/strong&gt; — The Gradle cache step should cut build times significantly after the first run. First build downloads all dependencies and can take 5-10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"No key with alias found in keystore"&lt;/strong&gt; — Double-check your &lt;code&gt;KEY_ALIAS&lt;/code&gt; secret matches exactly what you used when creating the keystore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Failed to read key from store"&lt;/strong&gt; — Your base64 encoding might be corrupted. Re-encode the keystore and update the secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pro Tips
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Concurrency groups&lt;/strong&gt; prevent wasted CI minutes. If you push twice quickly, the first run cancels automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tag-based triggers&lt;/strong&gt; for Play Store are ideal — push a &lt;code&gt;v1.0.0&lt;/code&gt; tag when you're ready to release, and CI handles the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firebase Distribution&lt;/strong&gt; is great for internal testing — testers get a notification with each new build, no Play Store review required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Easy Way
&lt;/h2&gt;

&lt;p&gt;Don't want to write YAML by hand? I built &lt;a href="https://runlane.dev" rel="noopener noreferrer"&gt;Run Lane&lt;/a&gt; — a free visual configurator that generates GitHub Actions workflows for Android and iOS. Pick your platform, choose your distribution target, and download a ready-to-use workflow. No account needed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: android, github, cicd, kotlin&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>cicd</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Deploy Your iOS App to TestFlight with GitHub Actions (Step-by-Step)</title>
      <dc:creator>Aleksandr Ilinskiy</dc:creator>
      <pubDate>Wed, 18 Feb 2026 20:15:56 +0000</pubDate>
      <link>https://forem.com/aleksandr_ilinskiy/how-to-deploy-your-ios-app-to-testflight-with-github-actions-step-by-step-4l1d</link>
      <guid>https://forem.com/aleksandr_ilinskiy/how-to-deploy-your-ios-app-to-testflight-with-github-actions-step-by-step-4l1d</guid>
      <description>&lt;p&gt;Setting up CI/CD for iOS is significantly harder than for web apps. You need macOS runners, the right Xcode version, code signing certificates, provisioning profiles, and App Store Connect API keys — all configured correctly in a YAML file.&lt;/p&gt;

&lt;p&gt;This guide walks you through the complete setup, from zero to a working GitHub Actions workflow that builds your app and uploads it to TestFlight on every push to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you start, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Apple Developer account ($99/year)&lt;/li&gt;
&lt;li&gt;Your app set up in App Store Connect&lt;/li&gt;
&lt;li&gt;An Xcode project that builds locally&lt;/li&gt;
&lt;li&gt;A GitHub repository&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create an App Store Connect API Key
&lt;/h2&gt;

&lt;p&gt;Apple deprecated password-based authentication. You need an API key.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://appstoreconnect.apple.com/access/api" rel="noopener noreferrer"&gt;App Store Connect → Users and Access → Keys&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Generate API Key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it "GitHub Actions CI" and give it &lt;strong&gt;Developer&lt;/strong&gt; role&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;.p8&lt;/code&gt; file (you can only download it once!)&lt;/li&gt;
&lt;li&gt;Note the &lt;strong&gt;Key ID&lt;/strong&gt; and &lt;strong&gt;Issuer ID&lt;/strong&gt; from the page&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: Export Your Code Signing Certificate
&lt;/h2&gt;

&lt;p&gt;You need your distribution certificate as a base64-encoded &lt;code&gt;.p12&lt;/code&gt; file.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Keychain Access&lt;/strong&gt; on your Mac&lt;/li&gt;
&lt;li&gt;Find your "Apple Distribution" certificate&lt;/li&gt;
&lt;li&gt;Right-click → &lt;strong&gt;Export&lt;/strong&gt; → save as &lt;code&gt;.p12&lt;/code&gt; with a password&lt;/li&gt;
&lt;li&gt;Base64-encode it:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; Certificates.p12 | pbcopy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This copies the encoded string to your clipboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add GitHub Secrets
&lt;/h2&gt;

&lt;p&gt;Go to your repo → &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt; and add these secrets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CERTIFICATES_P12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Base64-encoded .p12 certificate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CERTIFICATES_P12_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Password you set when exporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPSTORE_ISSUER_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;From App Store Connect API page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPSTORE_KEY_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;From App Store Connect API page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPSTORE_PRIVATE_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full contents of the .p8 file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPLE_TEAM_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your 10-character Team ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can find your Team ID at &lt;a href="https://developer.apple.com/account" rel="noopener noreferrer"&gt;developer.apple.com/account&lt;/a&gt; → Membership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Create the Workflow File
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/ios-testflight.yml&lt;/code&gt; in your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;iOS — Build &amp;amp; Deploy to TestFlight&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build &amp;amp; Upload to TestFlight&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-14&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache CocoaPods&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pods&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ runner.os }}-pods-&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install CocoaPods&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pod install --repo-update&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Select Xcode &lt;/span&gt;&lt;span class="m"&gt;15.2&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;maxim-lobanov/setup-xcode@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;xcode-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;15.2'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Import Code-Signing Certificate&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apple-actions/import-codesign-certs@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;p12-file-base64&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CERTIFICATES_P12 }}&lt;/span&gt;
          &lt;span class="na"&gt;p12-password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CERTIFICATES_P12_PASSWORD }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Import Provisioning Profile&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apple-actions/download-provisioning-profiles@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;bundle-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;com.yourcompany.yourapp'&lt;/span&gt;
          &lt;span class="na"&gt;issuer-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPSTORE_ISSUER_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;api-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPSTORE_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;api-private-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPSTORE_PRIVATE_KEY }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build &amp;amp; Archive&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;xcodebuild archive \&lt;/span&gt;
            &lt;span class="s"&gt;-scheme YourApp \&lt;/span&gt;
            &lt;span class="s"&gt;-configuration Release \&lt;/span&gt;
            &lt;span class="s"&gt;-archivePath $RUNNER_TEMP/YourApp.xcarchive \&lt;/span&gt;
            &lt;span class="s"&gt;-destination "generic/platform=iOS" \&lt;/span&gt;
            &lt;span class="s"&gt;CODE_SIGN_STYLE=Manual \&lt;/span&gt;
            &lt;span class="s"&gt;DEVELOPMENT_TEAM=${{ secrets.APPLE_TEAM_ID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Export IPA&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;xcodebuild -exportArchive \&lt;/span&gt;
            &lt;span class="s"&gt;-archivePath $RUNNER_TEMP/YourApp.xcarchive \&lt;/span&gt;
            &lt;span class="s"&gt;-exportPath $RUNNER_TEMP/export \&lt;/span&gt;
            &lt;span class="s"&gt;-exportOptionsPlist ExportOptions.plist&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload to TestFlight&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apple-actions/upload-testflight-build@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;app-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.temp }}/export/YourApp.ipa&lt;/span&gt;
          &lt;span class="na"&gt;issuer-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPSTORE_ISSUER_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;api-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPSTORE_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;api-private-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPSTORE_PRIVATE_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Replace &lt;code&gt;YourApp&lt;/code&gt; with your actual Xcode scheme name and &lt;code&gt;com.yourcompany.yourapp&lt;/code&gt; with your Bundle ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Create ExportOptions.plist
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;ExportOptions.plist&lt;/code&gt; in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;method&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;app-store&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;destination&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;export&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;signingStyle&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;manual&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;teamID&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;YOUR_TEAM_ID&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;uploadSymbols&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Push and Watch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add TestFlight CI/CD workflow"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to your repo → &lt;strong&gt;Actions&lt;/strong&gt; tab. You should see the workflow running. First build takes 10-15 minutes. If everything is configured correctly, your app will appear in TestFlight within 15-30 minutes after the build completes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Issues
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"No signing certificate found"&lt;/strong&gt; — Your P12 wasn't imported correctly. Make sure you base64-encoded the entire file and the password matches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"No provisioning profile found"&lt;/strong&gt; — Check that your Bundle ID in the workflow matches exactly what's in App Store Connect. Also verify your API key has the Developer role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"xcodebuild: error: The run destination is not valid"&lt;/strong&gt; — You're using the wrong macOS runner. Xcode 15.x needs &lt;code&gt;macos-14&lt;/code&gt;, Xcode 14.x needs &lt;code&gt;macos-13&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build succeeds but no TestFlight upload&lt;/strong&gt; — Check that &lt;code&gt;ExportOptions.plist&lt;/code&gt; has &lt;code&gt;method&lt;/code&gt; set to &lt;code&gt;app-store&lt;/code&gt; and your Team ID is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Easy Way
&lt;/h2&gt;

&lt;p&gt;If you don't want to write YAML by hand, I built &lt;a href="https://runlane.dev" rel="noopener noreferrer"&gt;Run Lane&lt;/a&gt; — a free visual configurator that generates these workflows for you. Pick iOS + TestFlight, fill in your scheme and Bundle ID, and download a ready-to-use workflow file. No account needed.&lt;/p&gt;

&lt;p&gt;It also supports Firebase Distribution and Android (Play Store, Firebase).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: ios, github, cicd, swift&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>github</category>
      <category>ios</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
