<?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>DEV Community: Munir Ahmad</title>
    <description>The latest articles on DEV Community by Munir Ahmad (@munirahmad6224).</description>
    <link>https://dev.to/munirahmad6224</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2482133%2F185824bd-394d-4a0b-b1e6-e6deaedf8b44.jpg</url>
      <title>DEV Community: Munir Ahmad</title>
      <link>https://dev.to/munirahmad6224</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/munirahmad6224"/>
    <language>en</language>
    <item>
      <title>Fastlane for Flutter: Complete Local Setup for iOS and Android Deployments</title>
      <dc:creator>Munir Ahmad</dc:creator>
      <pubDate>Mon, 14 Sep 2026 08:17:27 +0000</pubDate>
      <link>https://dev.to/munirahmad6224/fastlane-for-flutter-complete-local-setup-for-ios-and-android-deployments-i81</link>
      <guid>https://dev.to/munirahmad6224/fastlane-for-flutter-complete-local-setup-for-ios-and-android-deployments-i81</guid>
      <description>&lt;p&gt;Stop manual store clicks. Learn how to configure Fastlane locally to build, guard, and ship your Flutter app to TestFlight and Google Play with a single command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Forf3142gel1vfaaal3t2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Forf3142gel1vfaaal3t2.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Why bother
&lt;/h4&gt;

&lt;p&gt;Every Flutter release usually looks like this: bump the version, build the IPA, open Xcode, wait for the organizer, switch to browsers, build the AAB, drag-and-drop into the Play Console, and fill out release notes. It’s ten minutes of tedious clicking that changes every time, meaning it never becomes muscle memory.&lt;/p&gt;

&lt;p&gt;Fastlane turns that entire ritual into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;bundle exec fastlane ios deploy 
bundle exec fastlane android deploy
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This article is &lt;strong&gt;Part 1&lt;/strong&gt; : getting it working locally on your machine. Part 2 moves these exact lanes into GitHub Actions so a simple tag push handles the rest. Do this part first — the CI setup assumes your local lanes are already green.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you’ll need
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A Flutter app that already builds and runs&lt;/li&gt;
&lt;li&gt;An Apple Developer account ($99/yr) with the app registered in App Store Connect&lt;/li&gt;
&lt;li&gt;A Google Play Console account ($25 one-time) with the app created &lt;strong&gt;and at least one AAB uploaded manually&lt;/strong&gt;  — Play won’t accept the first-ever build over the API.&lt;/li&gt;
&lt;li&gt;Ruby (macOS ships with one; a version manager like rbenv is nicer but not required)&lt;/li&gt;
&lt;li&gt;Xcode with a valid signing setup for your app (Automatic signing with your team selected is fine for local)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m on FVM, so my Flutter SDK lives at &lt;code&gt;/Users/&amp;lt;you&amp;gt;/fvm/versions/3.32.0&lt;/code&gt;. Note your own path now — you'll need it in a minute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Install fastlane via a Gemfile (not globally)
&lt;/h3&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; brew install fastlane or gem install fastlane, but pinning it per-project with Bundler means your machine and your CI runner use the exact same version. Flutter projects have a ios/ and an android/ folder, and each gets its own Gemfile.&lt;/p&gt;

&lt;p&gt;ios/Gemfile&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"https://rubygems.org"&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"fastlane"&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"cocoapods"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;android/Gemfile&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"https://rubygems.org"&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"fastlane"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The iOS Gemfile also pins CocoaPods so the build doesn’t depend on whatever pod version happens to be on the system (this matters a lot on CI later). Android has no such dependency, so its Gemfile stays minimal.&lt;/p&gt;

&lt;p&gt;Install both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;cd ios &amp;amp;&amp;amp; bundle install &amp;amp;&amp;amp; cd ..
cd android &amp;amp;&amp;amp; bundle install &amp;amp;&amp;amp; cd ..
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here on, always call fastlane as bundle exec fastlane … so you get the pinned version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Set FLUTTER_ROOT
&lt;/h3&gt;

&lt;p&gt;fastlane’s iOS build scripts shell out to Flutter and need to know where the SDK is. Add this to your shell profile (&lt;code&gt;~/.zshrc&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;export FLUTTER_ROOT="/Users/&amp;lt;you&amp;gt;&lt;/span&gt;/fvm/versions/3.32.0&lt;span class="s2"&gt;"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you’re not on FVM, it’s wherever which flutter points, minus &lt;code&gt;/bin/flutter&lt;/code&gt;. Reload your shell (&lt;code&gt;source ~/.zshrc&lt;/code&gt;) and confirm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;echo $&lt;/span&gt;FLUTTER_ROOT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Confirm the raw builds work
&lt;/h3&gt;

&lt;p&gt;Before automating anything, make sure the two build commands fastlane will run actually succeed on their own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;fvm flutter build appbundle --release #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Android -&amp;gt; build/app/outputs/bundle/release/app-release.aab
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;fvm flutter build ipa --release #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;iOS -&amp;gt; build/ios/ipa/&amp;lt;YourApp&amp;gt;.ipa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;flutter build ipa&lt;/code&gt; complains about signing, open &lt;code&gt;ios/Runner.xcworkspace&lt;/code&gt; in Xcode once, select the Runner target → Signing &amp;amp; Capabilities, tick Automatically manage signing, pick your team, and let it create the profile. Then re-run. Don't move on until both commands produce an artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — &lt;code&gt;fastlane init&lt;/code&gt; for iOS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;from app&lt;span class="s1"&gt;'s root
&lt;/span&gt;&lt;span class="go"&gt;cd ios
bundle exec fastlane init
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick option 2 (Automate beta distribution to TestFlight). It parses your Xcode project, then asks you to log in with your Apple ID. This triggers a 2FA prompt on your trusted device — enter the 6-digit code. It’ll verify the app exists in both the Developer Portal and App Store Connect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feslxuph7wzly1x20pd0k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feslxuph7wzly1x20pd0k.png" width="800" height="786"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This creates &lt;code&gt;ios/fastlane/Appfile&lt;/code&gt; and &lt;code&gt;ios/fastlane/Fastfile&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 — &lt;code&gt;fastlane&lt;/code&gt; init for Android
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;from app&lt;span class="s1"&gt;'s root
&lt;/span&gt;&lt;span class="go"&gt;cd android
bundle exec fastlane init
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asks for your package name (must match applicationId in android/app/build.gradle), then asks for the path to a Play service account JSON. Press Enter to skip that for now — we'll wire it up properly in Step 6.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozldtxrh09gpmlydftrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fozldtxrh09gpmlydftrs.png" width="800" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This creates &lt;code&gt;android/fastlane/Appfile&lt;/code&gt; and &lt;code&gt;android/fastlane/Fastfile&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 — Fill in the Appfiles
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;android/fastlane/Appfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;json_key_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pc-key.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# only used by validate/supply — the deploy lane uses the base64 env var&lt;/span&gt;
&lt;span class="n"&gt;package_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"com.example.app"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# must match applicationId in android/app/build.gradle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now get the &lt;code&gt;pc-key.json&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com" rel="noopener noreferrer"&gt;Google Cloud APIs &amp;amp; Services page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Enable the Google Play Android Developer API for that project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv7xz1obpggdk94eoc186.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv7xz1obpggdk94eoc186.png" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Create the Service Account directly in Google Cloud&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://console.cloud.google.com/iam-admin/serviceaccounts" rel="noopener noreferrer"&gt;Google Cloud IAM Service Accounts page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Make sure you are logged into the Google account that manages this Play Store developer profile.&lt;/li&gt;
&lt;li&gt;Select your Google Cloud project from the top dropdown (or create a quick project named “Play Store Fastlane” if none exists).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ CREATE SERVICE ACCOUNT&lt;/strong&gt; at the top.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;fastlane-supply&lt;/code&gt; as the Service Account name and click  &lt;strong&gt;DONE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy the generated Service Account Email address&lt;/strong&gt; (looks like &lt;code&gt;fastlane-supply@your-project-id.iam.gserviceaccount.com&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Generate and download the JSON key:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click on the newly created service account email in the list.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Keys&lt;/strong&gt; tab at the top.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;ADD KEY&lt;/strong&gt; &amp;gt; &lt;strong&gt;Create new key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;JSON&lt;/strong&gt; as the key type and click  &lt;strong&gt;CREATE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Save the downloaded &lt;code&gt;.json&lt;/code&gt; file into your project's &lt;code&gt;android/&lt;/code&gt; directory as &lt;code&gt;pc-key.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Grant access via Users and permissions:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Return to your Google Play Console tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Users and permissions&lt;/strong&gt; from the left sidebar (the 3rd item from the top in your screenshot).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Invite new users&lt;/strong&gt; at the top right.&lt;/li&gt;
&lt;li&gt;Paste the Service Account Email you copied above into the email address field.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Account permissions&lt;/strong&gt; , grant permissions for &lt;strong&gt;Release to testing tracks&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Invite user&lt;/strong&gt; at the bottom right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verify the connection in Fastlane:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;bundle exec fastlane run validate_play_store_json_key json_key:pc-key.json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt27fe8k5699cr2e6g57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbt27fe8k5699cr2e6g57.png" width="800" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now base64-encode it into &lt;code&gt;.env.deploy&lt;/code&gt; (this is what the lane actually uses, and the same value goes into GitHub Secrets later — one encoding, two homes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;base64 -i android/pc-key.json | tr -d '\n' | pbcopy
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;paste &lt;/span&gt;into .env.deploy as: &lt;span class="nv"&gt;PLAY_STORE_JSON_KEY_BASE64&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;pasted&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ios/fastlane/Appfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;app_identifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"com.example.app"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# must match your bundle ID in Xcode&lt;/span&gt;
&lt;span class="n"&gt;apple_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"you@example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# your Apple Developer account email&lt;/span&gt;
&lt;span class="n"&gt;itc_team_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"1234567"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# App Store Connect team ID&lt;/span&gt;
&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ABCDE12345"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Developer Portal team ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don’t know the team IDs, run &lt;code&gt;bundle exec fastlane spaceship&lt;/code&gt; or check the URLs in the App Store Connect / Developer portal — they're in there. These aren't secret, but people often redact them out of habit; your call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7 — iOS auth: use an App Store Connect API key, not your password
&lt;/h3&gt;

&lt;p&gt;You’ll see guides that tell you to set &lt;code&gt;FASTLANE_USER&lt;/code&gt; and &lt;code&gt;FASTLANE_PASSWORD&lt;/code&gt;. Don't. With an Apple ID that has 2FA (all of them now do), that approach means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;constant 2FA prompts you can’t answer from a CI runner&lt;/li&gt;
&lt;li&gt;sessions that expire every few days&lt;/li&gt;
&lt;li&gt;the occasional locked account when Apple decides the login looks robotic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An App Store Connect API key sidesteps all of it — no 2FA, no expiry, and it’s scoped to just what it needs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;App Store Connect&lt;/strong&gt; → &lt;strong&gt;Users and Access&lt;/strong&gt; → &lt;strong&gt;Integrations&lt;/strong&gt; → &lt;strong&gt;App Store Connect API&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create a key with the App Manager role.&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;AuthKey_XXXXXXXXXX.p8&lt;/code&gt; — you can only download it once.&lt;/li&gt;
&lt;li&gt;Note the Key ID and the Issuer ID shown on that page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Encode the key and the two IDs into &lt;code&gt;.env.deploy&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;base64 -i AuthKey_XXXXXXXXXX.p8 | tr -d '\n' | pbcopy

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.env.deploy
&lt;span class="go"&gt;APP_STORE_CONNECT_API_KEY_ID=XXXXXXXXXX
APP_STORE_CONNECT_API_ISSUER_ID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
&lt;/span&gt;&lt;span class="gp"&gt;APP_STORE_CONNECT_API_KEY_BASE64=&amp;lt;pasted&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the downloaded &lt;code&gt;.p8&lt;/code&gt; once it's encoded — &lt;code&gt;.env.deploy&lt;/code&gt; is now the only copy you keep, and it's gitignored. Apple only lets you download the &lt;code&gt;.p8&lt;/code&gt; once. If you lose it, revoke that key in App Store Connect and issue a new one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7B — the &lt;code&gt;.env.deploy&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;All deploy secrets live in &lt;code&gt;foody-app/.env.deploy&lt;/code&gt;, which both Fastfiles load via &lt;code&gt;dotenv&lt;/code&gt; at the top. It is gitignored; a committed &lt;code&gt;.env.deploy.example&lt;/code&gt; documents every key. Your &lt;code&gt;.gitignore&lt;/code&gt; should already have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;.env&lt;/span&gt;
&lt;span class="err"&gt;.env.*&lt;/span&gt;
&lt;span class="c"&gt;!.env.example
!.env.deploy.example
&lt;/span&gt;&lt;span class="err"&gt;android/pc-key.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.env.deploy.example&lt;/code&gt; also lists &lt;code&gt;BUILD_CERTIFICATE_BASE64&lt;/code&gt;, &lt;code&gt;P12_PASSWORD&lt;/code&gt;, &lt;code&gt;BUILD_PROVISION_PROFILE_BASE64&lt;/code&gt;, &lt;code&gt;KEYCHAIN_PASSWORD&lt;/code&gt;, &lt;code&gt;APPLE_TEAM_ID&lt;/code&gt; — those are &lt;strong&gt;only for CI signing&lt;/strong&gt; (Part 2). Locally, Xcode's managed signing handles it and you can leave them blank.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8 — Write the deploy lanes
&lt;/h3&gt;

&lt;p&gt;Replace the generated &lt;code&gt;Fastfile&lt;/code&gt; in each folder with these.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fastlane init&lt;/code&gt; generates a placeholder &lt;code&gt;Fastfile&lt;/code&gt;. Replace each with the version below. They share the same shape: load &lt;code&gt;.env.deploy&lt;/code&gt;, &lt;strong&gt;check the store first&lt;/strong&gt; , build with &lt;code&gt;fvm flutter&lt;/code&gt;, upload.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;android/fastlane/Fastfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;default_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:android&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"dotenv"&lt;/span&gt;
&lt;span class="n"&gt;deploy_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../../.env.deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="no"&gt;Dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deploy_env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deploy_env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="ss"&gt;:android&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"Automated build &amp;amp; deploy to Play Store Internal Track"&lt;/span&gt;
  &lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:deploy&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;json_key_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;play_json_key_data&lt;/span&gt;
    &lt;span class="n"&gt;guard_version_code_is_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_key_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"🚀 Building Android App Bundle"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;Bundler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_unbundled_env&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cd ../.. &amp;amp;&amp;amp; fvm flutter build appbundle --release"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;upload_build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;json_key_data: &lt;/span&gt;&lt;span class="n"&gt;json_key_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"Upload the already-built AAB to Play Console (skips Flutter build)"&lt;/span&gt;
  &lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:upload&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;json_key_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;play_json_key_data&lt;/span&gt;
    &lt;span class="n"&gt;guard_version_code_is_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_key_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;upload_build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;json_key_data: &lt;/span&gt;&lt;span class="n"&gt;json_key_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;private_lane&lt;/span&gt; &lt;span class="ss"&gt;:upload_build&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;app_root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../.."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;aab_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"build/app/outputs/bundle/release/app-release.aab"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"No .aab found at &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;aab_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — run `fastlane android deploy` or `flutter build appbundle` first"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aab_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;upload_to_play_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;json_key_data: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:json_key_data&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;track: &lt;/span&gt;&lt;span class="s2"&gt;"internal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;aab: &lt;/span&gt;&lt;span class="n"&gt;aab_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;skip_upload_metadata: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;skip_upload_images: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;skip_upload_screenshots: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;play_json_key_data&lt;/span&gt;
  &lt;span class="n"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"PLAY_STORE_JSON_KEY_BASE64"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
  &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PLAY_STORE_JSON_KEY_BASE64 missing from foody-app/.env.deploy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"base64"&lt;/span&gt;
  &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pubspec_version&lt;/span&gt;
  &lt;span class="n"&gt;pubspec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../../pubspec.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pubspec&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^version:\s*(\d+\.\d+\.\d+)\+(\d+)/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Could not read version from pubspec.yaml"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;guard_version_code_is_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_key_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pubspec_version&lt;/span&gt;
  &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CredentialsManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AppfileConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;try_fetch_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:package_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[production internal]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flat_map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;begin&lt;/span&gt;
      &lt;span class="n"&gt;google_play_track_version_codes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;package_name: &lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;track: &lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;json_key_data: &lt;/span&gt;&lt;span class="n"&gt;json_key_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
      &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;important&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Could not read Play '&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' track (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:to_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;max_live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_live&lt;/span&gt;
    &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pubspec versionCode &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not ahead of Play's &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;max_live&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — run `cider bump build` first."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pubspec versionCode &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is ahead of Play (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;max_live&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) ✓"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ios/fastlane/Fastfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;default_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ios&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"dotenv"&lt;/span&gt;
&lt;span class="n"&gt;deploy_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../../.env.deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="no"&gt;Dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deploy_env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deploy_env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="ss"&gt;:ios&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"Build IPA and upload to TestFlight"&lt;/span&gt;
  &lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:deploy&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app_store_api_key&lt;/span&gt;
    &lt;span class="n"&gt;guard_build_number_is_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;Bundler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_unbundled_env&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cd .. &amp;amp;&amp;amp; fvm flutter build ipa --release"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;upload_build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;api_key: &lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"Upload the already-built IPA to TestFlight (skips the Flutter build)"&lt;/span&gt;
  &lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:upload&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app_store_api_key&lt;/span&gt;
    &lt;span class="n"&gt;guard_build_number_is_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;upload_build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;api_key: &lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;private_lane&lt;/span&gt; &lt;span class="ss"&gt;:upload_build&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;app_root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../.."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;ipa_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"build/ios/ipa/*.ipa"&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;max_by&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"No .ipa found under &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;app_root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/build/ios/ipa — run `fastlane ios deploy` or `flutter build ipa` first"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;ipa_path&lt;/span&gt;

    &lt;span class="n"&gt;upload_to_testflight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;api_key: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:api_key&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;ipa: &lt;/span&gt;&lt;span class="n"&gt;ipa_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;skip_waiting_for_build_processing: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;skip_submission: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;app_store_api_key&lt;/span&gt;
  &lt;span class="n"&gt;key_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"APP_STORE_CONNECT_API_KEY_ID"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
  &lt;span class="n"&gt;issuer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"APP_STORE_CONNECT_API_ISSUER_ID"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
  &lt;span class="n"&gt;key_b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"APP_STORE_CONNECT_API_KEY_BASE64"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;

  &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"APP_STORE_CONNECT_API_KEY_ID missing from foody-app/.env.deploy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
  &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"APP_STORE_CONNECT_API_ISSUER_ID missing from foody-app/.env.deploy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issuer_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
  &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"APP_STORE_CONNECT_API_KEY_BASE64 missing from foody-app/.env.deploy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key_b64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;

  &lt;span class="n"&gt;app_store_connect_api_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;key_id: &lt;/span&gt;&lt;span class="n"&gt;key_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;issuer_id: &lt;/span&gt;&lt;span class="n"&gt;issuer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;key_content: &lt;/span&gt;&lt;span class="n"&gt;key_b64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;is_key_content_base64: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;in_house: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pubspec_version&lt;/span&gt;
  &lt;span class="n"&gt;pubspec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../../pubspec.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pubspec&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^version:\s*(\d+\.\d+\.\d+)\+(\d+)/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Could not read version from pubspec.yaml"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Fail before the ~10-minute build if pubspec's +N was not bumped past what&lt;/span&gt;
&lt;span class="c1"&gt;# TestFlight already has for this marketing version.&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;guard_build_number_is_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pubspec_version&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;latest_testflight_build_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;api_key: &lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;app_identifier: &lt;/span&gt;&lt;span class="no"&gt;CredentialsManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AppfileConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;try_fetch_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:app_identifier&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;version: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;initial_build_number: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Could not check TestFlight build numbers (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) — skipping the guard; the store will still reject a reused build."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;live&lt;/span&gt;
    &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pubspec build &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not ahead of TestFlight's &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — run `cider bump build` first."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pubspec &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is ahead of TestFlight (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) ✓"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env.deploy&lt;/code&gt; at the top &lt;strong&gt;.&lt;/strong&gt; Both files load it with &lt;code&gt;dotenv&lt;/code&gt; before anything runs. On CI the same variables come from the environment, so the Fastfile doesn't change.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;guard_*&lt;/code&gt; check runs first, and hits the store API, not a build. &lt;code&gt;flutter build ipa&lt;/code&gt; takes ~10 minutes. If you forgot to bump the version, the guard catches it in a couple of seconds — iOS asks TestFlight for the latest build number of the current marketing version; Android asks Play for the highest &lt;code&gt;versionCode&lt;/code&gt; across the &lt;code&gt;production&lt;/code&gt; and &lt;code&gt;internal&lt;/code&gt; tracks. If your &lt;code&gt;pubspec.yaml&lt;/code&gt; isn't ahead, it stops with &lt;em&gt;"run&lt;/em&gt; &lt;em&gt;&lt;code&gt;cider bump build&lt;/code&gt; first."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_store_api_key&lt;/code&gt; / &lt;code&gt;play_json_key_data&lt;/code&gt; decode the base64 secret in memory. Nothing is ever written to disk.&lt;/li&gt;
&lt;li&gt;Two lanes. &lt;code&gt;deploy&lt;/code&gt; = build + upload. &lt;code&gt;upload&lt;/code&gt; = upload the artifact that's already in &lt;code&gt;build/&lt;/code&gt;, skipping the build — for when the upload failed and you don't want to wait 10 minutes again.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Bundler.with_unbundled_env { sh("… fvm flutter build …") }&lt;/code&gt; runs the Flutter build outside fastlane's Bundler context so Flutter/CocoaPods resolve against FVM cleanly.&lt;/li&gt;
&lt;li&gt;iOS picks the newest &lt;code&gt;build/ios/ipa/*.ipa&lt;/code&gt; by modified time rather than guessing the filename from the app's display name. It uploads with &lt;code&gt;skip_submission&lt;/code&gt; (goes to TestFlight, not the App Store review queue) and &lt;code&gt;skip_waiting_for_build_processing&lt;/code&gt; (don't block the terminal while Apple processes it).&lt;/li&gt;
&lt;li&gt;Android uploads to the &lt;code&gt;internal&lt;/code&gt; track and skips metadata/images/screenshots so the lane only touches the binary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 9 — Bump the version and Changelog
&lt;/h3&gt;

&lt;p&gt;Play and TestFlight both reject a build number that already exists. pubspec.yaml’s version: line — X.Y.Z+N — is the single source of truth for both the iOS CFBundleVersion and the Android versionCode, and +N is the value both stores check for uniqueness:&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0+1&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;marketing version&amp;gt; + &amp;lt;build number&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This project manages the version with &lt;a href="https://pub.dev/packages/cider" rel="noopener noreferrer"&gt;cider&lt;/a&gt;, added as a dev-dependency:&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;dev_dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.2.10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s a dependency, not a global tool, so run it through the SDK: &lt;code&gt;fvm dart run cider&lt;/code&gt; …&lt;/p&gt;

&lt;h4&gt;
  
  
  While you work: log changes
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;cider&lt;/code&gt; collects entries under an &lt;code&gt;## Unreleased&lt;/code&gt; heading in &lt;code&gt;CHANGELOG.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;fvm dart run cider log added "Dark mode toggle in settings."
fvm dart run cider log fixed "Crash when opening an expired invite link."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;log&lt;/code&gt; subcommands: &lt;code&gt;added&lt;/code&gt;, &lt;code&gt;changed&lt;/code&gt;, &lt;code&gt;deprecated&lt;/code&gt;, &lt;code&gt;fixed&lt;/code&gt;, &lt;code&gt;removed&lt;/code&gt;, &lt;code&gt;security&lt;/code&gt;.)&lt;/p&gt;

&lt;h4&gt;
  
  
  At release time: bump once, then release
&lt;/h4&gt;

&lt;p&gt;Step 1 — bump the version. Run one of these, never both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;fvm dart run cider bump build #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;same version, next build: 1.0.0+1 -&amp;gt; 1.0.0+2
&lt;span class="gp"&gt;fvm dart run cider bump patch --bump-build #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;new marketing version: 1.0.0+1 -&amp;gt; 1.0.1+2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;minor and major work the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;cider bump minor --bump-build
cider bump major --bump-build
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always pass --bump-build on a marketing bump. A plain cider bump patch drops the +N build number, and Play Console rejects any upload whose versionCode isn’t higher than the last one shipped. To set a version explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;fvm dart run cider version 1.4.0+37.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2 — stamp the changelog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;fvm dart run cider release
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cider release turns &lt;code&gt;##Unreleased&lt;/code&gt; into a dated heading. After cider bump build &lt;code&gt;(-&amp;gt;1.0.0+2)&lt;/code&gt; and the two log entries above, CHANGELOG.md becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 1.0.0+2 - 2026-09-10&lt;/span&gt;
&lt;span class="gu"&gt;### Added&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Dark mode toggle in settings.

&lt;span class="gu"&gt;### Fixed&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Crash when opening an expired invite link.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit just those two files and tag — the tag is what Part 2’s CI triggers on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;git add pubspec.yaml CHANGELOG.md
git commit -m "chore(release): v1.0.0+2"
git tag -a v1.0.0+2 -m "v1.0.0+2"
git push --follow-tags
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this once per release, from a clean tree, then deploy both platforms from that commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 10 — Ship it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;from app&lt;span class="s1"&gt;'s root
&lt;/span&gt;&lt;span class="go"&gt;cd android &amp;amp;&amp;amp; bundle exec fastlane deploy &amp;amp;&amp;amp; cd ..
cd ios &amp;amp;&amp;amp; bundle exec fastlane deploy &amp;amp;&amp;amp; cd ..
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both lanes read &lt;code&gt;foody-app/.env.deploy&lt;/code&gt;. If a build fails at upload, rerun just &lt;code&gt;bundle exec fastlane &amp;lt;platform&amp;gt; upload&lt;/code&gt; — it skips the 10-minute rebuild.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting the first run
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;flutter&lt;/code&gt; or &lt;code&gt;fvm: command not found&lt;/code&gt; inside a lane: Ensure FVM is added to your shell's PATH where you launch Fastlane.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;invalid curve name&lt;/code&gt; / &lt;code&gt;.p8&lt;/code&gt; key errors: Your Base64-encoded key string contains hidden line breaks. Re-encode it cleanly:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;base64 -i key.p8 | tr -d '\n'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;versionCode N is not ahead of Play's M&lt;/code&gt;: You skipped the version bump step. Run &lt;code&gt;fvm dart run cider bump patch --bump-build&lt;/code&gt;, commit, and retry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*_missing&lt;/code&gt; from &lt;code&gt;.env.deploy&lt;/code&gt; errors: Fastlane cannot locate your environment file. Ensure &lt;code&gt;.env.deploy&lt;/code&gt; sits directly at your app's root directory (&lt;code&gt;foody-app/.env.deploy&lt;/code&gt;), not inside &lt;code&gt;ios/&lt;/code&gt; or &lt;code&gt;android/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What’s next — Part 2
&lt;/h3&gt;

&lt;p&gt;Part 2 moves this exact workflow to GitHub Actions. Because every secret is already a base64 string in &lt;code&gt;.env.deploy&lt;/code&gt;, CI is mostly “same variables, sourced from GitHub Secrets instead of the file.” The additions are: the iOS signing vars from &lt;code&gt;.env.deploy.example&lt;/code&gt; (&lt;code&gt;BUILD_CERTIFICATE_BASE64&lt;/code&gt;, &lt;code&gt;P12_PASSWORD&lt;/code&gt;, &lt;code&gt;BUILD_PROVISION_PROFILE_BASE64&lt;/code&gt;, &lt;code&gt;KEYCHAIN_PASSWORD&lt;/code&gt;), a step that builds a throwaway keychain on the runner, and a workflow triggered by the &lt;code&gt;v*&lt;/code&gt; tag you push in Step 9. The Fastfiles don't change at all.&lt;/p&gt;

&lt;p&gt;Ten minutes of tedious web console clicking are now permanently replaced by a single command. Your local release pipeline is fully automated, version-guarded, and bulletproof.&lt;/p&gt;

&lt;p&gt;Up next in &lt;strong&gt;Part 2&lt;/strong&gt; , we’ll take these exact lanes, move them to the cloud, and wire them up to GitHub Actions so a simple tag push handles the rest. Let’s ship code faster! 🚀&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>fastlane</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>Use Multiple GitHub Accounts on One Machine (SSH)</title>
      <dc:creator>Munir Ahmad</dc:creator>
      <pubDate>Sun, 14 Dec 2025 15:01:47 +0000</pubDate>
      <link>https://dev.to/munirahmad6224/how-to-manage-multiple-github-accounts-on-one-machine-using-ssh-keys-1kio</link>
      <guid>https://dev.to/munirahmad6224/how-to-manage-multiple-github-accounts-on-one-machine-using-ssh-keys-1kio</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F460upyqtorhqq7y9ansb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F460upyqtorhqq7y9ansb.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A step-by-step guide to seamlessly manage multiple GitHub accounts with SSH keys.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Managing Multiple GitHub Accounts on One Machine Using SSH
&lt;/h3&gt;

&lt;p&gt;If you use both personal and work GitHub accounts on the same machine, authentication issues are almost inevitable. Commits go to the wrong account, pushes fail, or Git keeps asking for credentials.&lt;/p&gt;

&lt;p&gt;The clean solution is simple: &lt;strong&gt;one SSH key per account, configured once, and reused forever&lt;/strong&gt;. This guide shows how to do exactly that.&lt;/p&gt;
&lt;h3&gt;
  
  
  What You’ll Learn
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create separate SSH keys for each GitHub account&lt;/li&gt;
&lt;li&gt;Configure SSH to automatically select the correct identity&lt;/li&gt;
&lt;li&gt;Clone, push, and commit without account conflicts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Applies to:&lt;/strong&gt; macOS and Linux (Windows users via WSL or Git Bash can follow the same steps)&lt;/p&gt;
&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Git installed&lt;/li&gt;
&lt;li&gt;Basic terminal knowledge&lt;/li&gt;
&lt;li&gt;Access to multiple GitHub accounts&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 1: Generate a Dedicated SSH Key
&lt;/h3&gt;

&lt;p&gt;Create a new key for your work account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-keygen -t ed25519 -C "work@email.com" -f ~/.ssh/id_ed25519_work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids overwriting existing keys and clearly separates identities.&lt;br&gt;&lt;br&gt;
When prompted, adding a passphrase is recommended for security.&lt;/p&gt;

&lt;p&gt;Generated files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;id_ed25519_work — private key (keep secret)&lt;/li&gt;
&lt;li&gt;id_ed25519_work.pub — public key (add to GitHub)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 2: Add the Key to the SSH Agent
&lt;/h3&gt;

&lt;p&gt;Start the agent and load your key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows Git to authenticate without repeatedly asking for a passphrase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add the Public Key to GitHub
&lt;/h3&gt;

&lt;p&gt;Copy your public key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat ~/.ssh/id_ed25519_work.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the entire output (starts with ssh-ed25519).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to: &lt;a href="https://github.com/settings/keys" rel="noopener noreferrer"&gt;&lt;strong&gt;https://github.com/settings/keys&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New SSH key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add a title (e.g., “Work Laptop — 2024”)&lt;/li&gt;
&lt;li&gt;Paste your public key&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add SSH key&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9g6r8mn6npzs3p23jowm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9g6r8mn6npzs3p23jowm.png" alt=" " width="800" height="293"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Highlighting where to add your SSH key in GitHub settings.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Configure SSH Host Aliases
&lt;/h3&gt;

&lt;p&gt;Edit your SSH config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano ~/.ssh/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Here you can add multiple identities personal, work etc.
# Personal account
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes

# Work account
Host github-work
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_work
  IdentitiesOnly yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells SSH exactly which key to use for each account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Test the Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh -T github-work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If successful, GitHub will confirm authentication. Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hi username! You've successfully authenticated, but GitHub does not provide shell access.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using the Correct Account
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Clone Repositories
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Work account
git clone git@github-work:company/repo.git

# Personal account
git clone git@github.com:username/repo.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fix an Existing Repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git remote set-url origin git@github-work:company/repo.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Set Commit Identity (Per Repository)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git config user.name "Your Name"
git config user.email "work@company.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures commits are attributed correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Managing multiple GitHub accounts doesn’t require hacks or workarounds.&lt;br&gt;&lt;br&gt;
With &lt;strong&gt;one SSH key per account and a clear SSH configuration&lt;/strong&gt; , Git becomes invisible — and that’s exactly how tooling should behave.&lt;/p&gt;

&lt;p&gt;Configure it once. Forget about it forever.&lt;/p&gt;

</description>
      <category>gitconfiguration</category>
      <category>github</category>
      <category>sshkeys</category>
      <category>multipleaccounts</category>
    </item>
    <item>
      <title>Secure Swagger &amp; ReDoc in Django REST Framework with DRF Spectacular: Best Practices for API…</title>
      <dc:creator>Munir Ahmad</dc:creator>
      <pubDate>Sun, 07 Dec 2025 17:56:18 +0000</pubDate>
      <link>https://dev.to/munirahmad6224/secure-swagger-redoc-in-django-rest-framework-with-drf-spectacular-best-practices-for-api-332a</link>
      <guid>https://dev.to/munirahmad6224/secure-swagger-redoc-in-django-rest-framework-with-drf-spectacular-best-practices-for-api-332a</guid>
      <description>&lt;h3&gt;
  
  
  Secure Swagger &amp;amp; ReDoc in Django REST Framework with DRF Spectacular: Best Practices for API Security
&lt;/h3&gt;

&lt;p&gt;API documentation tools like &lt;strong&gt;Swagger UI&lt;/strong&gt; and &lt;strong&gt;ReDoc&lt;/strong&gt; make development easier, but they also expose &lt;strong&gt;every endpoint, schema, and request structure&lt;/strong&gt; to anyone who can access the URL.&lt;/p&gt;

&lt;p&gt;If you deploy your Django REST Framework project without protecting /api/docs/ or /api/schema/, you're unintentionally giving outsiders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A complete map of all your APIs&lt;/li&gt;
&lt;li&gt;All request/response structures&lt;/li&gt;
&lt;li&gt;Authentication patterns&lt;/li&gt;
&lt;li&gt;Internal admin/debug endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes exploitation easier and increases your attack surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That’s why Swagger must never be public in production.&lt;/strong&gt; In this guide, we’ll show a clean, secure way to protect your API documentation using &lt;strong&gt;DRF Spectacular&lt;/strong&gt; in a Django REST Framework project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7mty7b8cfjz281n48e2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7mty7b8cfjz281n48e2.png" alt=" " width="720" height="480"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A secure architecture for safeguarding Swagger and ReDoc in DRF using DRF Spectacular.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Directory structure
core/
    docs/
        swagger_view.py
    urls.py
    settings.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;core/docs/swagger_view.py&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.shortcuts import redirect
from drf_spectacular.views import (
    SpectacularAPIView,
    SpectacularSwaggerView,
    SpectacularRedocView,
)

class AdminOnlyMixin:
    """
    Restrict access to authenticated superusers.
    """

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return redirect(f"/admin/login/?next={request.path}")
        if not request.user.is_superuser:
            return redirect("/403/") # Make sure a 403 page exists
        return super().dispatch(request, *args, **kwargs)

class SuperuserSchemaView(AdminOnlyMixin, SpectacularAPIView):
    pass

class SuperuserSwaggerView(AdminOnlyMixin, SpectacularSwaggerView):
    pass

class SuperuserRedocView(AdminOnlyMixin, SpectacularRedocView):
    pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;core/urls.y&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.urls import path
from core.docs import (
    SuperuserRedocView,
    SuperuserSchemaView,
    SuperuserSwaggerView,
)

urlpatterns = [
    # API documentation (protected)
    path("api/schema/", SuperuserSchemaView.as_view(), name="schema"),
    path("api/docs/", SuperuserSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
    path("api/redoc/", SuperuserRedocView.as_view(url_name="schema"), name="redoc"),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;core/settings.py&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        # JWT for external API clients
        "rest_framework_simplejwt.authentication.JWTAuthentication",

        # Session auth so Swagger works automatically after admin login
        "rest_framework.authentication.SessionAuthentication",
    ),
    "DEFAULT_PERMISSION_CLASSES": (
        "rest_framework.permissions.IsAuthenticated",
    ),
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

SPECTACULAR_SETTINGS = {
    "TITLE": "Your API",
    "DESCRIPTION": "API documentation",
    "VERSION": "1.0.0",

    # Allow our custom view to control permissions manually
    "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"],

    # Use DRF’s normal authentication classes (Session + JWT)
    "SERVE_AUTHENTICATION": None,

    # Important: Do NOT expose schema automatically in unrestricted mode
    "SERVE_INCLUDE_SCHEMA": False,
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SessionAuthentication allows admin users to access protected docs easily.&lt;/li&gt;
&lt;li&gt;SERVE_INCLUDE_SCHEMA = False ensures your schema isn’t automatically exposed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that’s the real value of securing your API documentation: you empower your team without empowering your attackers. When Swagger and ReDoc live behind proper authentication, your developers gain clarity, your system gains integrity, and your attack surface quietly shrinks. Secure the docs, protect the blueprint, and let your API evolve with confidence — exactly the way production-grade engineering should be. Appreciate your time. Until next time 👋.&lt;/p&gt;

</description>
      <category>django</category>
      <category>djangorestframework</category>
      <category>drfspectacular</category>
      <category>restapi</category>
    </item>
    <item>
      <title>Install the Latest PostgreSQL (psql 18) on Ubuntu 24.04 (Noble)</title>
      <dc:creator>Munir Ahmad</dc:creator>
      <pubDate>Mon, 03 Nov 2025 17:40:16 +0000</pubDate>
      <link>https://dev.to/munirahmad6224/install-the-latest-postgresql-psql-18-on-ubuntu-2404-noble-3fdp</link>
      <guid>https://dev.to/munirahmad6224/install-the-latest-postgresql-psql-18-on-ubuntu-2404-noble-3fdp</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why I Wrote This&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While setting up my development environment on &lt;strong&gt;Ubuntu 24.04 (Noble Numbat)&lt;/strong&gt;, I noticed the default PostgreSQL version in Ubuntu’s repository was outdated.&lt;br&gt;&lt;br&gt;
So I decided to document the clean, official way to install the latest &lt;strong&gt;PostgreSQL (v18) &lt;/strong&gt;— straight from the PostgreSQL Apt repository.&lt;/p&gt;

&lt;p&gt;If you’ve ever run into issues like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;“No cluster suitable as a default target”&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;or old versions like PostgreSQL 14 or 15 being installed instead of the latest then this guide will help you start fresh.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Step-by-Step Guide to Installing PostgreSQL 18&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4u6tijhx6istsf2xo48k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4u6tijhx6istsf2xo48k.png" alt=" " width="720" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 1 — Update and Upgrade Your System&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2 — Check Your Ubuntu &lt;strong&gt;Codename:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL’s official repo depends on your Ubuntu version codename.&lt;br&gt;&lt;br&gt;
 Run the following to check yours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;. /etc/os-release
echo $VERSION_CODENAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Ubuntu 24.04, the output should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;noble
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3 — Install Required Packages&lt;/p&gt;

&lt;p&gt;We’ll install curl, ca-certificates, and postgresql-common:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt install -y curl ca-certificates postgresql-common
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4 — Add the PostgreSQL Apt Repository&lt;/p&gt;

&lt;p&gt;Run the following command to automatically add the official PostgreSQL repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 5 — Update Package Lists &amp;amp; Install PostgreSQL 18&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update
sudo apt install -y postgresql-18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted during installation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type &lt;strong&gt;Y&lt;/strong&gt; to confirm.&lt;/li&gt;
&lt;li&gt;If you see a purple configuration screen like this 👇 choose
&lt;strong&gt;“install the package maintainer’s version”&lt;/strong&gt;
(This ensures a clean, default setup for PostgreSQL 18.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkga71btjf6gqnxq0dpt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkga71btjf6gqnxq0dpt.png" alt=" " width="720" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 7 — Verify Installation&lt;/p&gt;

&lt;p&gt;Check the installed version you should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;psql --version
psql (PostgreSQL) 18.x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;That’s it! You’ve successfully installed and verified PostgreSQL 18 on Ubuntu 24.04 (Noble).&lt;br&gt;&lt;br&gt;
Your system is now running the latest, officially supported PostgreSQL version, ready for use in production or local development.If you encounter any issues, check the PostgreSQL service logs with:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo journalctl -u postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Otherwise — you’re all set to start building with PostgreSQL 18.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
  </channel>
</rss>
