<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Vedant Shirke on Medium]]></title>
        <description><![CDATA[Stories by Vedant Shirke on Medium]]></description>
        <link>https://medium.com/@vedantshirke?source=rss-ffa5310e0b5e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*XWfMigykQBbDLWe9.</url>
            <title>Stories by Vedant Shirke on Medium</title>
            <link>https://medium.com/@vedantshirke?source=rss-ffa5310e0b5e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 17 Oct 2026 09:26:12 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@vedantshirke/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How to Set Up Git Branch and Commit Rules in 10 Minutes]]></title>
            <link>https://medium.com/@vedantshirke/how-to-set-up-git-branch-and-commit-rules-in-10-minutes-c40cb3bd236b?source=rss-ffa5310e0b5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/c40cb3bd236b</guid>
            <category><![CDATA[github]]></category>
            <category><![CDATA[git]]></category>
            <category><![CDATA[npm]]></category>
            <category><![CDATA[nodejs]]></category>
            <dc:creator><![CDATA[Vedant Shirke]]></dc:creator>
            <pubDate>Wed, 14 Oct 2026 09:38:52 GMT</pubDate>
            <atom:updated>2026-10-14T09:41:44.230Z</atom:updated>
            <content:encoded><![CDATA[<h4>A hands-on guide to installing @agnotic/git-guardian and enforcing team conventions with Git hooks</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0ELeB5LlB3aqRkYNPqr7ww.png" /></figure><p>In my <a href="https://medium.com/@vedantshirke/i-built-git-guardian-to-stop-repeating-the-same-git-reviews-ec434f57f77a?sharedUserId=vedantshirke">previous article</a>, I wrote about why I built Git Guardian: I wanted to stop spending code review time on branch names and commit message formats.</p><p>This article is a hands-on follow-up. I’ll install the package in a fresh repository, try an invalid branch name, create a valid branch, and make a commit. You can follow along in your own project.</p><h3>Start with a test repository</h3><p>You’ll need Node.js 20 or later and Git 2.28 or later. Git Guardian is an npm package, so the repository also needs a package.json.</p><p>For a quick test, create a new directory:</p><pre>mkdir git-guardian-demo<br>cd git-guardian-demo<br>npm init -y<br>git init</pre><p>Install the scoped package:</p><pre>npm install --save-dev @agnotic/git-guardian</pre><p>Then run the setup wizard:</p><pre>npx git-guardian init</pre><p>The wizard creates a .git-guardian.yml file and installs the Git hooks. You can press Enter to accept the defaults.</p><p>With the default settings, branch names follow this format:</p><pre>&lt;type&gt;/&lt;number&gt;-&lt;description&gt;</pre><p>For example:</p><pre>feat/123-add-login</pre><p>The allowed branch and commit types default to feat, chore, bugfix, and task.</p><h3>Check that the hooks are installed</h3><p>Run the doctor command:</p><pre>npx git-guardian doctor</pre><p>Here’s the kind of output I saw:</p><pre>Git Guardian doctor<br>✓ Repository: /path/to/git-guardian-demo<br>✓ Config: /path/to/git-guardian-demo/.git-guardian.yml<br>✓ Git 2.51.1 supports branch transaction guarding.<br>✓ Secret scanning is on for commits.<br>→ Hooks directory: /path/to/git-guardian-demo/.git/hooks<br>✓ reference-transaction hook installed and executable.<br>✓ commit-msg hook installed and executable.<br>✓ Everything looks good.</pre><p>The two hooks have different jobs:</p><ul><li>reference-transaction checks branch creation.</li><li>commit-msg checks commit messages and, when enabled, scans staged changes for common secret patterns.</li></ul><h3>See what happens to an invalid branch</h3><p>Try creating a branch that doesn’t follow the configured format:</p><pre>git switch -c wip/login</pre><p>Git Guardian stops the branch from being created and explains why:</p><pre>✗ Branch creation blocked: wip/login<br>Branch &quot;wip/login&quot; doesn&#39;t follow the format &lt;type&gt;/&lt;number&gt;-&lt;description&gt;<br>&quot;wip&quot; is not an allowed type.</pre><pre>Format:  &lt;type&gt;/&lt;number&gt;-&lt;description&gt;<br>  &lt;type&gt;         one of: feat, chore, bugfix, task<br>  &lt;number&gt;       a number, e.g. 123<br>  &lt;description&gt;  lowercase words joined by hyphens, e.g. add-login<br>Example: feat/123-add-login</pre><pre>Try:<br>  feat/&lt;number&gt;-wip-login<br>  chore/&lt;number&gt;-wip-login<br>  bugfix/&lt;number&gt;-wip-login<br>  (replace any &lt;placeholder&gt; left in a suggestion)<br>fatal: ref updates aborted by hook</pre><p>The branch is rejected before Git creates it. The error also shows which part of the name failed and gives examples of the required format.</p><p>Now create a valid branch:</p><pre>git switch -c feat/123-add-login</pre><p>This time, Git switches to the new branch.</p><h3>Make a commit</h3><p>Create a small file, stage it, and commit it:</p><pre>echo &quot;Hello from the tutorial&quot; &gt; example.txt<br>git add example.txt<br>git commit -m &quot;feat: add tutorial example&quot;</pre><p>The commit message matches the default format, so Git records the commit.</p><p>You can also check a name or message without trying to create a branch or commit:</p><pre>npx git-guardian validate branch feat/123-add-login<br>npx git-guardian validate commit -m &quot;feat: add tutorial example&quot;</pre><p>And if you have a ticket title or rough description, ask for branch suggestions:</p><pre>npx git-guardian branch suggest &quot;ABC-42 Fix login crash&quot;</pre><p>Example output:</p><pre>Suggested branch names<br>  bugfix/42-login-crash<br>  feat/42-login-crash<br>  chore/42-login-crash</pre><h3>Test the secret check safely</h3><p>With secret scanning enabled, Git Guardian checks staged changes for common patterns. To see it work, use a fake token in a throwaway test file, never paste a real credential into your repository:</p><pre>echo &quot;ghp_123456789012345678901234567890123456&quot; &gt; fake-token.txt<br>git add fake-token.txt<br>git commit -m &quot;chore: test secret detection&quot;</pre><p>The commit is blocked, and the detected value is masked:</p><pre>✗ Commit blocked by Git Guardian: possible secrets found.<br>  fake-token.txt:1  GitHub token  ghp_************</pre><pre>What to do:<br>  Remove the value and load it from an environment variable or secret manager instead.</pre><p>This is a local guardrail for common patterns. It isn’t a replacement for your Git host’s secret scanning or push protection. If a real credential has already been pushed, revoke and rotate it.</p><p>After the test, remove the staged test file:</p><pre>git restore --staged fake-token.txt<br>rm fake-token.txt</pre><h3>Where the rules live</h3><p>The wizard writes the policy to .git-guardian.yml. A default configuration looks like this:</p><pre>version: 1<br>branch:<br>  enabled: true<br>  format: &quot;&lt;type&gt;/&lt;number&gt;-&lt;description&gt;&quot;<br>  types: &quot;feat,chore,bugfix,task&quot;<br>  protected: &quot;main,master,develop,development,staging,production&quot;<br>  suggestions: true<br>commit:<br>  enabled: true<br>  format: &quot;&lt;type&gt;: &lt;description&gt;&quot;<br>  types: &quot;feat,chore,bugfix,task&quot;<br>  maxSubjectLength: 72<br>  autoTicket: true<br>secrets:<br>  enabled: true<br>  allow: &quot;&quot;<br>hooks:<br>  referenceTransaction: true<br>  commitMsg: true</pre><p>You can commit this file so the policy is shared with your team. The hook files are installed locally in each clone, so teammates need to set them up after pulling the repository:</p><pre>npm install<br>npx git-guardian install<br>npx git-guardian doctor</pre><h3>What the test showed</h3><p>In a fresh repository, the main flow is short: install the package, run init, check the hooks with doctor, and try a branch and commit.</p><p>The useful part is when a rule fails. Instead of discovering a naming problem during a pull request, the developer gets feedback in the terminal while creating the branch or committing.</p><p>For team use, add matching checks to CI as well. Local hooks can be skipped with --no-verify, and every clone needs its own hook installation.</p><p>Try it with your own conventions:</p><pre>npm install --save-dev @agnotic/git-guardian<br>npx git-guardian init<br>npx git-guardian doctor</pre><ul><li><a href="https://www.npmjs.com/package/@agnotic/git-guardian">Git Guardian on npm</a></li><li><a href="https://github.com/vedant-agnotic/guardian-engineering-toolkit">Guardian Engineering Toolkit on GitHub</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c40cb3bd236b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Built git-guardian to Stop Repeating the Same Git Reviews]]></title>
            <link>https://medium.com/@vedantshirke/i-built-git-guardian-to-stop-repeating-the-same-git-reviews-ec434f57f77a?source=rss-ffa5310e0b5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/ec434f57f77a</guid>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[git]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[developer-tools]]></category>
            <category><![CDATA[open-source]]></category>
            <dc:creator><![CDATA[Vedant Shirke]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 14:54:54 GMT</pubDate>
            <atom:updated>2026-10-13T14:54:54.656Z</atom:updated>
            <content:encoded><![CDATA[<h4>Enforce branch naming, commit formats, and secret scanning with a zero-regex, config-driven npm tool.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/512/1*LxUKjn65VtZW3efFE4ry_A.png" /><figcaption>git-guardian</figcaption></figure><p><a href="https://www.npmjs.com/package/@agnotic/git-guardian?activeTab=readme">Explore git-guardian</a></p><p>A pull request should be an engineering conversation about the code: architecture, edge cases, and performance.<br>Yet far too often, the very first review comment is:</p><blockquote>“Please rename the branch to match our Jira ticket format.”</blockquote><p>The second comment is:</p><blockquote>“Can you update the commit message to follow Conventional Commits?”</blockquote><p>Neither comment improves the code. Both interrupt the review flow. And when senior engineers spend cognitive energy playing syntax police across five different repositories, it becomes an exhausting chore.</p><p>I kept hitting this while leading mobile, web, and backend engineering teams. We had documented conventions. We held onboarding sessions. Yet adherence still relied on human memory, peer vigilance, and manual corrections.</p><p>That frustration led me to build <strong>@agnotic/git-guardian</strong>: an opinionated, batteries-included Git validation tool designed to eliminate conventions friction once and for all.</p><h3>The Problem Is Bigger Than Untidy Names</h3><p>A branch named login-fix makes sense to its author on Tuesday afternoon. A commit message that says &quot;fixed stuff&quot; feels adequate when the diff is fresh in your head.</p><p>Three months later, an engineer is debugging a production incident or bisecting a regression. That shorthand now gives them nothing to work with.</p><p><strong>Compare:</strong></p><pre>Branch: loginfix<br>Commit: fixed</pre><p><strong>With:</strong></p><pre>Branch: fix/AUTH-123-expired-token<br>Commit: fix(AUTH-123): handle expired access tokens gracefully</pre><p>The second version provides instant situational awareness. It links the change to a Jira/Linear ticket, defines the functional domain, and describes the actual behavior modified.</p><p>Consistent naming won’t solve every engineering challenge, but it eradicates avoidable ambiguity. The real challenge is making that consistency <strong>effortless to follow</strong>.</p><h3>What Git Guardian Does Differently</h3><p>Most teams attempt to solve this by stitching together custom bash scripts, husky, and commitlint. While powerful, this setup usually involves maintaining fragile regular expressions, boilerplate hook files, and separate linters.</p><p>With git-guardian, the developer experience is built around five core pillars:</p><h4>1. Human-Readable Formats (No Regular Expressions)</h4><p>You don’t configure complex regex patterns. Conventions are declared with plain placeholders in .git-guardian.yml:</p><pre>branch:<br>  format: &quot;&lt;type&gt;/&lt;ticket&gt;-&lt;description&gt;&quot;<br>  types: &quot;feat,chore,fix,task&quot;<br>commit:<br>  format: &quot;&lt;type&gt;(&lt;ticket&gt;): &lt;description&gt;&quot;<br>  autoTicket: true</pre><h4>2. Native Git Branch Guarding</h4><p>Unlike tools that only run at commit time, Git Guardian utilizes Git’s native reference-transaction hook. If an engineer runs git switch -c bad-name or git checkout -b temp, Git Guardian intercepts the action <strong>before the branch is even created</strong>, displays what failed, and suggests valid names based on what they typed.</p><h4>3. Automatic Ticket Enrichment</h4><p>Developers often forget to repeat their ticket ID in commit messages. When commit.autoTicket: true is enabled, Git Guardian automatically detects the ticket (e.g., AUTH-123) from the current branch name and injects it into the commit message on the fly.</p><h4>4. Guided Interactive Commit Wizard</h4><p>Don’t want teammates to memorize rules? They can just run:</p><pre>npx git-guardian commit</pre><p>An interactive prompt asks for the type, ticket, and summary, formats the message according to your repo rules, and runs the commit cleanly.</p><h4>5. Native Secret Scanning on Commit</h4><p>Before any commit is recorded, staged diffs and messages are scanned for AWS keys, OpenAI tokens, private certificates, and committed .env files. If a credential is detected, the commit is blocked locally with masked diagnostics.</p><h3>Why Immediate Local Feedback Matters</h3><p>When a naming error is flagged in a GitHub pull request, the developer has already switched context to their next task. They have to context-switch back, rename branches, rewrite Git history, force-push, and ping reviewers again.</p><p>Local Git hooks bring that feedback to the exact second the mistake occurs. The context is fresh, the correction takes five seconds, and the PR arrives clean.</p><h3>Getting Started</h3><p>In any Git repository (Node, React Native, Python, Go, or Rust):</p><pre># 1. Install as a dev dependency<br>npm install --save-dev @agnotic/git-guardian<br><br># 2. Interactive guided setup (creates .git-guardian.yml &amp; sets up hooks)<br>npx git-guardian init<br><br># 3. Verify health and hook integrity<br>npx git-guardian doctor</pre><p>Commit the generated .git-guardian.yml file. When teammates pull the repo and run npm install, they simply run npx git-guardian install once to wire their local hooks.</p><h3>Where It Fits Alongside Husky and commitlint</h3><p>Husky and commitlint are staples of the JavaScript ecosystem. Husky is a generalized Git hook runner; commitlint is a dedicated commit message linter.</p><p>git-guardian was engineered for teams that want <strong>an all-in-one, config-driven convention guardian</strong>:</p><ul><li><strong>Branch validation</strong> is built-in natively (Husky doesn’t validate branch creation out of the box).</li><li><strong>No regex authoring</strong> or complex JavaScript config files.</li><li><strong>Auto-ticket injection and secret scanning</strong> come included without chaining external plugins or lint-staged scripts.</li><li><strong>Zero runtime dependencies.</strong></li></ul><p>If your team already has a battle-tested Husky + commitlint pipeline that everyone loves, keep it! But if you find yourself repeatedly copying 50 lines of bash scripts and regexes between projects, Git Guardian offers a streamlined alternative.</p><h3>What I Want This Project to Achieve</h3><p>My goal is simple: <strong>zero PR comments spent on housekeeping.</strong></p><p>Every minute spent reminding an engineer about branch syntax or commit style is a minute diverted from discussing code quality, architecture, and user experience.</p><p>@agnotic/git-guardian is the first package in the <strong>Guardian Engineering Toolkit, </strong>an initiative aimed at standardizing developer guardrails across Git workflows, CI quality pipelines, and release engineering.</p><p>Try it out on your next project, and let me know: What conventions does your team enforce, and where does your Git workflow still cause friction?</p><p><a href="https://www.npmjs.com/package/@agnotic/git-guardian?activeTab=readme">Explore git-guardian</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ec434f57f77a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[NavigationStack vs Custom Router: What Actually Scales in Real iOS Apps?]]></title>
            <link>https://medium.com/dev-beyond/navigationstack-vs-custom-router-what-actually-scales-in-real-ios-apps-8cd59b67ac04?source=rss-ffa5310e0b5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/8cd59b67ac04</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[swiftui]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[ios-development]]></category>
            <dc:creator><![CDATA[Vedant Shirke]]></dc:creator>
            <pubDate>Wed, 10 Dec 2025 15:12:42 GMT</pubDate>
            <atom:updated>2025-12-10T15:12:42.361Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Djn2CmFT2Dl3laCfui1CAA.png" /></figure><p>I’ve been building SwiftUI apps long enough to accept one truth:<br><strong>SwiftUI navigation works beautifully… until it doesn’t.</strong></p><p>When your app is small, NavigationStack feels magical.<br>Once you start adding deep linking, authentication flows, modals, tabs, and modular features… well, the magic doesn’t last long.</p><p>If you’ve read my previous article — <br> 👉 <strong>I Was Tired of Fighting SwiftUI Navigation, So I Built My Own Router</strong><br> <a href="https://medium.com/@vedantshirke/i-was-tired-of-fighting-swiftui-navigation-so-i-built-my-own-router-199a5fecf81c">https://medium.com/@vedantshirke/i-was-tired-of-fighting-swiftui-navigation-so-i-built-my-own-router-199a5fecf81c</a><br> — you already know exactly why I felt the need to rethink SwiftUI navigation entirely.</p><p>This article takes that discussion further.<br>Let’s break down the actual truth:</p><p><strong>Which navigation approach truly scales in a real production SwiftUI app?</strong></p><h3>Why NavigationStack Feels Perfect at First</h3><p>SwiftUI’s NavigationStack gives you a clean and declarative way to manage navigation:</p><pre>NavigationStack {<br>    List(items) { item in<br>        NavigationLink(value: item) {<br>            Text(item.name)<br>        }<br>    }<br>    .navigationDestination(for: Item.self) { item in<br>        ItemDetailView(item: item)<br>    }<br>}</pre><p>This is ideal for:</p><ul><li>simple list → detail flows</li><li>small apps</li><li>prototypes</li><li>demos</li><li>non-nested navigation scenarios</li></ul><p>In the beginning, everything feels great.<br>Then your app grows.</p><h3>Where NavigationStack Starts Cracking (Real Production Issues)</h3><p>SwiftUI demos rarely show what happens once your app crosses 10+ screens.<br>But that’s exactly when NavigationStack begins to show its limitations.</p><h3>1. No Central Control</h3><p>You can’t easily:</p><ul><li>reset navigation</li><li>clear a flow</li><li>trigger navigation from a ViewModel</li><li>coordinate multiple screens cleanly</li></ul><p>Everything is tied to the UI layer.</p><h3>2. Deep Linking Gets Messy Fast</h3><p>When handling:</p><ul><li>universal links,</li><li>notifications,</li><li>dynamic routing,</li><li>and fallback flows,</li></ul><p>navigation becomes a scattered web of conditions.</p><h3>3. Feature Modules Cannot Own Their Navigation</h3><p>If you’re working with modular architecture, navigation becomes tightly coupled with parent views.<br>Clean architecture? Hard to maintain.</p><h3>4. Hard to Test and Reason About</h3><p>Navigation logic spreads across views.<br>ViewModels start leaking UI concerns.<br>Testing becomes painful.</p><p>This is exactly why many teams end up creating their own solution.</p><h3>Enter Custom Routers (The Practical, Scalable Approach)</h3><p>A <strong>Custom Router</strong> gives you:</p><ul><li>a single navigation source of truth</li><li>testable and predictable routes</li><li>full programmatic control</li><li>clean flow resets</li><li>modular independence</li><li>a consistent API for navigation</li></ul><p>Instead of the UI owning navigation state, the <strong>router</strong> owns it.</p><p>Example:</p><pre>class AppRouter: ObservableObject {<br>    @Published var path = NavigationPath()<br>     <br>    func push(_ route: Route) { <br>        path.append(route) <br>    }<br>    <br>    func pop() {<br>        path.removeLast()<br>    }<br><br>    func popToRoot() {<br>        path.removeLast(path.count)<br>    }<br>}</pre><p>Views simply <strong>observe</strong> navigation.<br>Your architecture remains stable as the app grows.</p><h3>Where SwiftUIRouter (SwiftUIRouterKit) Fits In</h3><p>After repeatedly hitting SwiftUI’s navigation limitations across different projects, I finally built <strong>SwiftUIRouter</strong> — a lightweight router package for SwiftUI apps that need predictable, scalable navigation.</p><h3>What SwiftUIRouter solves:</h3><ul><li>Navigation logic becomes centralized</li><li>ViewModels can navigate cleanly</li><li>No navigation leaks into UI</li><li>Flows become modular &amp; testable</li><li>You don’t need to reinvent routing from scratch</li></ul><p>In short:</p><blockquote><strong><em>SwiftUIRouter gives you all the benefits of a custom router without the overhead of building one manually.</em></strong></blockquote><p>It’s intentionally small, production-focused, and easy to integrate.</p><h3>When Should You Use What?</h3><h3>Use NavigationStack if:</h3><ul><li>You’re building a very small app</li><li>Flows are simple</li><li>No deep linking is needed</li><li>It’s a prototype or MVP</li></ul><h3>Use a Custom Router if:</h3><ul><li>Your app has more than one flow</li><li>You need clean architecture</li><li>You want to support deep links</li><li>You want centralized navigation</li></ul><h3>Use SwiftUIRouter if:</h3><ul><li>You want the power of a router</li><li>Without maintaining routing infrastructure</li><li>While keeping SwiftUI code clean and modular</li><li>And ensuring production scalability</li></ul><h3>A Real-World Architecture That Scales</h3><pre>App<br> ├── AppRouter (from SwiftUIRouter)<br> ├── AuthFlow<br> ├── DashboardFlow<br> ├── ProfileFlow<br> ├── SettingsFlow<br> └── Shared Components</pre><p>Each flow manages its own routes.<br>Navigation logic stays simple and predictable.<br>The app stays maintainable even after months of growth and feature additions.</p><h3>Final Verdict</h3><p><strong>NavigationStack is amazing for small apps.<br>Custom routers are amazing for real-world apps.<br>SwiftUIRouter gives you both power and simplicity.</strong></p><p>SwiftUI’s default navigation works great until you start shipping real, non-trivial apps.<br>Once you’re dealing with deep linking, multiple flows, modular architecture, and state-driven navigation, you quickly realize how much easier life is with a proper router.</p><p>And if you want to understand why I built SwiftUIRouter in the first place, here’s the original story:</p><p>👉 <a href="https://medium.com/@vedantshirke/i-was-tired-of-fighting-swiftui-navigation-so-i-built-my-own-router-199a5fecf81c">https://medium.com/@vedantshirke/i-was-tired-of-fighting-swiftui-navigation-so-i-built-my-own-router-199a5fecf81c</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8cd59b67ac04" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dev-beyond/navigationstack-vs-custom-router-what-actually-scales-in-real-ios-apps-8cd59b67ac04">NavigationStack vs Custom Router: What Actually Scales in Real iOS Apps?</a> was originally published in <a href="https://medium.com/dev-beyond">Dev Beyond</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Was Tired of Fighting SwiftUI Navigation — So I Built My Own Router]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/@vedantshirke/i-was-tired-of-fighting-swiftui-navigation-so-i-built-my-own-router-199a5fecf81c?source=rss-ffa5310e0b5e------2"><img src="https://cdn-images-1.medium.com/max/1536/1*4Clzk1pyWQT_ZWQfojoxQg.png" width="1536"></a></p><p class="medium-feed-snippet">SwiftUI made building UI easy.</p><p class="medium-feed-link"><a href="https://medium.com/@vedantshirke/i-was-tired-of-fighting-swiftui-navigation-so-i-built-my-own-router-199a5fecf81c?source=rss-ffa5310e0b5e------2">Continue reading on Medium »</a></p></div>]]></description>
            <link>https://medium.com/@vedantshirke/i-was-tired-of-fighting-swiftui-navigation-so-i-built-my-own-router-199a5fecf81c?source=rss-ffa5310e0b5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/199a5fecf81c</guid>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[ios-apps]]></category>
            <category><![CDATA[swift-programming]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[swiftui]]></category>
            <dc:creator><![CDATA[Vedant Shirke]]></dc:creator>
            <pubDate>Mon, 08 Dec 2025 07:04:03 GMT</pubDate>
            <atom:updated>2025-12-08T07:04:03.613Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Automate iOS Testing & Reporting with ios-test-runner – Now on Homebrew!]]></title>
            <link>https://medium.com/@vedantshirke/automate-ios-testing-reporting-with-ios-test-runner-now-on-homebrew-a173253edf3f?source=rss-ffa5310e0b5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/a173253edf3f</guid>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[mobile-testing]]></category>
            <dc:creator><![CDATA[Vedant Shirke]]></dc:creator>
            <pubDate>Thu, 07 Aug 2025 05:06:17 GMT</pubDate>
            <atom:updated>2025-08-07T05:06:17.613Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OAzM2EVhohwe1vJSCUJ3Lw.png" /></figure><blockquote>Run tests on Simulator, generate JUnit/HTML reports, and streamline your iOS testing process with one CLI tool.</blockquote><h4>📦 Introduction</h4><p>Automated testing is the backbone of scalable iOS development. But running tests manually via Xcode, resetting simulators, and collecting result reports can become tedious — especially in CI environments.</p><p>That’s why I built <a href="https://github.com/ENVY-Packages/homebrew-envy-ios-test-runner">ios-test-runner</a>, a lightweight <strong>command-line tool</strong> that lets you:</p><ul><li>✅ Run iOS tests via xcodebuild</li><li>✅ Automatically boot/reset simulators</li><li>✅ Generate <strong>HTML</strong>, <strong>JUnit XML</strong>, and <strong>text</strong> reports from .xcresult</li><li>✅ Use it both <strong>locally</strong> and in <strong>CI/CD pipelines</strong></li><li>✅ Install easily via <strong>Homebrew</strong></li></ul><h4>🍺 Install via Homebrew</h4><p>You can now install ios-test-runner as a <strong>Homebrew package</strong> in just two steps:</p><pre>brew tap ENVY-Packages/homebrew-envy-ios-test-runner<br>brew install ios-test-runner</pre><p>And that’s it — you’re ready to go.</p><h4>🚀 Quick Usage</h4><p>Run the tool like this:</p><pre>ios-test-runner -s MyAppScheme -d &quot;iPhone 15&quot; -o test-output</pre><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/c54dbf5a130f35f7ba86468449ed0961/href">https://medium.com/media/c54dbf5a130f35f7ba86468449ed0961/href</a></iframe><h3>⚙️ What Does It Do?</h3><p>Here’s a breakdown of what ios-test-runner automates:</p><h4>✅ 1. Dependency Check</h4><p>Before running, it ensures essential tools are available:</p><pre>xcodebuild<br>xcrun<br>xcresultparser</pre><blockquote><em>Don’t have </em><em>xcresultparser? Install it via: </em><em>brew install xcresultparser</em></blockquote><h4>🌀 2. Resets &amp; Boots Simulator</h4><p>No more simulator clutter or stale states. The tool:</p><ul><li>Kills running simulators</li><li>Resets them cleanly</li><li>Boots the selected one automatically</li></ul><h4>🧪 3. Executes Tests via xcodebuild</h4><p>It runs your test suite using:</p><pre>xcodebuild test \<br>  -scheme &quot;$SCHEME&quot; \<br>  -destination &quot;platform=iOS Simulator,name=$DEVICE,OS=latest&quot; \<br>  -enableCodeCoverage YES \<br>  -resultBundlePath TestResults.xcresult</pre><h4>📄 4. Generates Rich Reports</h4><p>After tests run, it uses xcresultparser to convert .xcresult into:</p><ul><li>📄 report.html – clean test summary in browser</li><li>📄 junit.xml – for CI/CD integrations</li><li>📄 output.txt – plain-text summary</li></ul><p>These are saved in the directory you provide via -o.</p><h3>📁 Example Output</h3><p>After execution, you’ll have a structured output like:</p><pre>reporter/reports/<br>├── report.html<br>├── junit.xml<br>└── output.txt</pre><p>You can open the HTML file in your browser or use junit.xml with Jenkins, GitHub Actions, or GitLab.</p><h3>🧰 Perfect for CI Pipelines</h3><p>Add it to your CI pipeline like this:</p><pre>- name: Install Test Runner<br>  run: |<br>    brew tap ENVY-Packages/homebrew-envy-ios-test-runner<br>    brew install ios-test-runner<br><br>- name: Run iOS Tests<br>  run: ios-test-runner -s MyAppScheme<br></pre><p>This makes your tests more <strong>reproducible</strong>, <strong>report-friendly</strong>, and <strong>CI-ready</strong>.</p><h3>🔄 Update Instructions</h3><p>You can always update the tool with:</p><pre>brew update<br>brew upgrade ios-test-runner</pre><h3>💬 Final Thoughts</h3><p>Whether you’re a solo iOS dev or working with a larger team, ios-test-runner simplifies repetitive test workflows and ensures consistent reporting.</p><p>Install it once via Homebrew, and you’ll never go back to running raw xcodebuild commands again.</p><p>Happy testing! 🚀</p><p><a href="https://img.shields.io/badge/brew%20install-ios--test--runner-brightgreen?style=for-the-badge&amp;logo=homebrew)](https://github.com/ENVY-Packages/homebrew-envy-ios-test-runner)">https://img.shields.io/badge/brew%20install-ios--test--runner-brightgreen?style=for-the-badge&amp;logo=homebrew</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a173253edf3f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ iOS CI/CD with GitHub Actions: TestFlight Deployment on Release (Part 2)]]></title>
            <link>https://medium.com/@vedantshirke/ios-ci-cd-with-github-actions-testflight-deployment-on-release-part-2-89bfe8301144?source=rss-ffa5310e0b5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/89bfe8301144</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[mobile-devops]]></category>
            <category><![CDATA[firebase]]></category>
            <dc:creator><![CDATA[Vedant Shirke]]></dc:creator>
            <pubDate>Wed, 16 Jul 2025 05:14:20 GMT</pubDate>
            <atom:updated>2025-07-16T05:14:20.602Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*emDwNLRmE6wlkdXyUbTbCg.png" /></figure><p>In <a href="https://medium.com/@vedantshirke/ios-ci-cd-with-github-actions-firebase-deployment-on-push-trigger-part-1-d85ba9d68bfe">Part 1</a>, we automated iOS builds and distributed them to Firebase App Distribution on every push to main, develop, or release/*. It was all about <strong>continuous delivery for testers</strong>.</p><p>In <strong>Part 2</strong>, we’re moving into <strong>production territory</strong> — we’ll level up by automating deployments to <strong>TestFlight</strong> using GitHub Actions triggered by a <strong>GitHub Release</strong> event.</p><p>This workflow adds several enhancements:<br>1. Automatically generates a <strong>unique build number<br>2. </strong>Commits the updated build number and Info.plist to the correct branch<br>3. Force pushes the <strong>tag associated with the GitHub Release<br>4. </strong>Uploads the build to <strong>TestFlight</strong> and Firebase</p><p>Since this is a continuation of Part 1, we’ll keep common steps like CocoaPods installation, Fastlane setup, and Firebase CLI brief — and <strong>focus on what’s new and more advanced in this part</strong>.</p><h3>🧱 Pipeline Overview (Part 2)</h3><p><strong>Trigger:</strong> GitHub Release (release.published)</p><p><strong>Steps:<br></strong>1. Automatically <strong>generate a unique build number</strong><br>2. Commit updated build number back to the repo<br>3. Force push a tag corresponding to the release<br>4. Upload to <strong>TestFlight</strong> with retry logic</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NJZd2fBzy-E80OfRJMBbAw.png" /><figcaption>GitHub Release trigger → Build number generation → Code signing and build → Upload to TestFlight and Firebase.</figcaption></figure><h3>⚙️ Job 1: Set Up Environment and Build Number</h3><h4>Trigger</h4><pre>on:<br>  release:<br>    types: [published]</pre><h4>🔢 Key Addition: Generate Build Number (Unique Per Day)</h4><p>This step is new in Part 2. We generate a unique build number based on the current date (YYYYMMDD) followed by a counter (01 to 99) to avoid duplicates on the same day:</p><pre>DATE=$(date &#39;+%Y%m%d&#39;)<br>BUILD_FILE=&quot;build_number.txt&quot;<br>if [ -f &quot;$BUILD_FILE&quot; ]; then<br>  LAST_BUILD=$(cat &quot;$BUILD_FILE&quot;)<br>  LAST_DATE=${LAST_BUILD:0:8}<br>  LAST_COUNTER=${LAST_BUILD:8:2}<br>else<br>  LAST_DATE=&quot;&quot;<br>  LAST_COUNTER=0<br>fi<br><br>if [ &quot;$LAST_DATE&quot; = &quot;$DATE&quot; ]; then<br>  COUNTER=$(printf &quot;%02d&quot; $((10#$LAST_COUNTER + 1)))<br>else<br>  COUNTER=&quot;01&quot;<br>fi<br><br>if [ &quot;$COUNTER&quot; -gt 99 ]; then<br>  echo &quot;Build number counter exceeded 99 for today ($DATE)&quot;<br>  exit 1<br>fi<br>NEW_BUILD_NUMBER=&quot;${DATE}${COUNTER}&quot;<br>echo &quot;$NEW_BUILD_NUMBER&quot; &gt; build_number.txt</pre><p><strong>Why?<br></strong>1. Ensures every build in a day has a unique, ascending number<br>2. Avoids manual version bumping</p><p>This build number is passed as an output to downstream jobs and injected into the app’s Info.plist.</p><h3>🧱 Job 2: Build the iOS App</h3><blockquote>⚠️ This section shares many steps with Part 1. Refer back if you want details on CocoaPods installation, code signing, or Fastlane setup.</blockquote><h4>✅ Key Difference: Updating Build Number and Tag</h4><ol><li><strong>Update Build Number in </strong><strong>Info.plist</strong></li></ol><pre>/usr/libexec/PlistBuddy -c &quot;Set :CFBundleVersion $BUILD_NUMBER&quot; Info.plist<br>echo &quot;$BUILD_NUMBER&quot; &gt; build_number.txt</pre><p><strong>2. Extract Target Branch from Release</strong></p><pre>- name: Extract Target Branch<br>  id: extract_branch<br>  run: echo &quot;branch=${{ github.event.release.target_commitish }}&quot; &gt;&gt; $GITHUB_OUTPUT</pre><p><strong>3. Commit Build Number and Push Updated Tag</strong> This is one of the most critical parts of this workflow.</p><pre>git checkout $TARGET_BRANCH<br>git pull origin $TARGET_BRANCH<br><br>git add build_number.txt Info.plist<br>git commit -m &quot;🔢 Update build number to $BUILD_NUMBER&quot; || echo &quot;No changes&quot;<br>git push origin $TARGET_BRANCH<br><br># Re-tag the release to include updated build<br>git tag -f $RELEASE_TAG<br>git push origin $RELEASE_TAG --force</pre><p>This ensures that the tag pointing to the release always references the correct commit with the proper build number and versioning.</p><h4>📦 Archive and Export IPA</h4><p>Refer to <a href="https://medium.com/@vedantshirke/ios-ci-cd-with-github-actions-firebase-deployment-on-push-trigger-part-1-d85ba9d68bfe">Part 1</a> for the full clean + archive + export steps. Nothing changes here.</p><h4>🧠 Upload Symbols to Crashlytics</h4><pre>- name: Upload dSYM to Firebase Crashlytics<br> run: |<br> Pods/FirebaseCrashlytics/upload-symbols -gsp &lt;APP_NAME&gt;/Firebase/${{ env.PLIST_FILE_NAME }} -p ios &lt;APP_NAME&gt;.xcarchive/dSYMs/${{ env.PRODUCT_NAME }}.app.dSYM</pre><h3>🚀 Job 3: Deploy to TestFlight and Firebase</h3><p>The upload logic here is nearly identical to Part 1, but with some improvements:</p><p><strong>1. Install Fastlane and Firebase CLI</strong></p><pre>sudo gem install fastlane -NV<br>curl -sL https://firebase.tools | bash</pre><p><strong>2. Download IPA Artifact</strong></p><pre>- uses: actions/download-artifact@v4<br>  with:<br>    name: &lt;APP_NAME&gt;<br>    path: .</pre><p><strong>3. Create App Store Connect API Key JSON:</strong> Before uploading to TestFlight, Fastlane requires authentication using an App Store Connect API key. We dynamically create the required AuthKey.json file:</p><pre>cat &lt;&lt;EOF &gt; fastlane/AuthKey.json<br>{<br>  &quot;key_id&quot;: &quot;${{ secrets.PROD_KEY_ID }}&quot;,<br>  &quot;issuer_id&quot;: &quot;${{ secrets.PROD_ISSUER_ID }}&quot;,<br>  &quot;key&quot;: &quot;${{ secrets.PROD_KEY }}&quot;<br>}<br>EOF</pre><p><strong>4. Upload to TestFlight with Retry Logic: </strong>This step uploads the .ipa file to <strong>TestFlight</strong> using Fastlane. Since network issues or App Store Connect hiccups are not uncommon, a <strong>retry mechanism</strong> is used to improve reliability.</p><pre>for i in {1..3}; do<br>  echo &quot;🔁 Attempt $i to upload to TestFlight&quot;<br>  if bundle exec fastlane ios upload_only; then<br>    echo &quot;✅ Upload succeeded&quot;<br>    break<br>  elif [ &quot;$i&quot; -eq 3 ]; then<br>    echo &quot;❌ Upload failed after 3 attempts&quot;<br>    exit 1<br>  else<br>    echo &quot;⏳ Waiting before retry...&quot;<br>    sleep 30<br>  fi<br>done</pre><ol><li>It tries uploading up to <strong>3 times</strong>.</li><li>If the upload fails, it <strong>waits 30 seconds</strong> before retrying.</li><li>If it still fails after 3 attempts, the workflow <strong>fails gracefully</strong>.</li></ol><p>This logic improves deployment reliability by avoiding transient failures.</p><p><strong>5. Fastfile: </strong>upload_only<strong> Lane</strong></p><p>Here’s the Fastlane lane used in the upload job:</p><pre>platform :ios do<br>  lane :upload_only do<br>    upload_to_testflight(<br>      ipa: &quot;&lt;APP_NAME&gt;.ipa&quot;,<br>      api_key_path: &quot;fastlane/AuthKey.json&quot;<br>    )<br>  end<br>end</pre><p>This lane does the following:</p><p>1. Uploads the exported IPA (&lt;APP_NAME&gt;.ipa) to TestFlight<br>2. Authenticates with App Store Connect using the API key JSON file<br>3. Keeps the lane minimal — ideal for CI/CD workflows</p><p><strong>6. Publish to Firebase App Distribution</strong></p><pre>firebase appdistribution:distribute &lt;APP_NAME&gt;.ipa \<br>  --app $FIREBASE_APP_ID \<br>  --groups &quot;PROD&quot; \<br>  --release-notes &quot;$RELEASE_BODY&quot; \<br>  --token $FIREBASE_TOKEN</pre><h3>✅ What You Achieve</h3><p>On every published GitHub Release:</p><p>1. 🔢 Build number is auto-generated and committed<br>2. 🏷️ Git tag is force-updated to latest commit<br>3. 📦 App is signed, archived &amp; uploaded<br>4. 🚀 Delivered to TestFlight and Firebase<br>5. 🧠 dSYMs uploaded for Crashlytics</p><p>Fully automated. Fully controlled.</p><h3>📚 Summary</h3><p>This workflow helps eliminate all manual steps after tagging a release. You can now:</p><p>1. Trigger builds via GitHub UI by creating a release<br>2. Keep version control accurate and reproducible<br>3. Simplify QA workflows with both Firebase and TestFlight deploys</p><p>With this, you’ve built a robust, end-to-end iOS CI/CD pipeline — and while this might be the last part for now, it’s just the beginning of what you can automate.</p><p>🔗 <a href="https://medium.com/@vedantshirke/ios-ci-cd-with-github-actions-firebase-deployment-on-push-trigger-part-1-d85ba9d68bfe">Missed Part 1? Read it here</a></p><pre>name: Build And Upload To Testflight<br><br>on:<br>  release:<br>    types: [published]<br><br>jobs:<br><br>  # ──────────────────────────────<br>  # Job 1: Environment Setup<br>  # ──────────────────────────────<br>  setup:<br>    name: Set Up Environment<br>    runs-on: macos-latest<br>    outputs:<br>      build_number: ${{ steps.generate_build.outputs.BUILD_NUMBER }}<br>      build_env: PROD<br>      firebase_app_id_secret_name: FIREBASE_APP_ID<br>      firebase_token: FIREBASE_TOKEN<br>      provisioning_profile_name: DIST_PROVISIONING_PROFILE<br>      provisioning_profile_specifier_name: &lt;PROFILE_NAME&gt;<br>      scheme_name: &lt;SCHEME_NAME&gt;<br>      product_name: &lt;APP_NAME&gt;<br>      plist_file_name: GoogleService-Info.plist<br><br>    steps:<br>      # Checkout the repo<br>      - name: Checkout Source Code<br>        uses: actions/checkout@v4<br><br>      # Set up Ruby environment for CocoaPods<br>      - name: Setup Ruby Environment<br>        uses: ruby/setup-ruby@v1<br>        with:<br>          ruby-version: &#39;3.1.0&#39;<br><br>      # Install CocoaPods dependencies<br>      - name: Install CocoaPods<br>        run: |<br>          gem install cocoapods<br>          pod install<br><br>      # Generate unique build number: YYYYMMDD + counter (01-99)<br>      - name: Generate Build Number<br>        id: generate_build<br>        run: |<br>          DATE=$(date &#39;+%Y%m%d&#39;)<br>          BUILD_FILE=&quot;build_number.txt&quot;<br>          if [ -f &quot;$BUILD_FILE&quot; ]; then<br>            LAST_BUILD=$(cat &quot;$BUILD_FILE&quot;)<br>            LAST_DATE=${LAST_BUILD:0:8}<br>            LAST_COUNTER=${LAST_BUILD:8:2}<br>          else<br>            LAST_DATE=&quot;&quot;<br>            LAST_COUNTER=0<br>          fi<br><br>          if [ &quot;$LAST_DATE&quot; = &quot;$DATE&quot; ]; then<br>            COUNTER=$(printf &quot;%02d&quot; $((10#$LAST_COUNTER + 1)))<br>          else<br>            COUNTER=&quot;01&quot;<br>          fi<br><br>          if [ &quot;$COUNTER&quot; -gt 99 ]; then<br>            echo &quot;Build number counter exceeded 99 for today ($DATE)&quot;<br>            exit 1<br>          fi<br><br>          NEW_BUILD_NUMBER=&quot;${DATE}${COUNTER}&quot;<br>          echo &quot;$NEW_BUILD_NUMBER&quot; &gt; &quot;$BUILD_FILE&quot;<br>          echo &quot;BUILD_NUMBER=$NEW_BUILD_NUMBER&quot; &gt;&gt; $GITHUB_ENV<br>          echo &quot;BUILD_NUMBER=$NEW_BUILD_NUMBER&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;<br>          echo &quot;✅ Build Number: $NEW_BUILD_NUMBER&quot;<br><br>  # ──────────────────────────────<br>  # Job 2: Build the iOS App<br>  # ──────────────────────────────<br>  build:<br>    name: Build App<br>    runs-on: macos-latest<br>    needs: setup<br>    env:<br>      BUILD_ENV: ${{ needs.setup.outputs.build_env }}<br>      FIREBASE_APP_ID_SECRET_NAME: ${{ needs.setup.outputs.firebase_app_id_secret_name }}<br>      FIREBASE_TOKEN: ${{ needs.setup.outputs.firebase_token }}<br>      PROVISIONING_PROFILE_NAME: ${{ needs.setup.outputs.provisioning_profile_name }}<br>      PROVISIONING_PROFILE_SPECIFIER_NAME: ${{ needs.setup.outputs.provisioning_profile_specifier_name }}<br>      SCHEME_NAME: ${{ needs.setup.outputs.scheme_name }}<br>      PRODUCT_NAME: ${{ needs.setup.outputs.product_name }}<br>      PLIST_FILE_NAME: ${{ needs.setup.outputs.plist_file_name }}<br>      BUILD_NUMBER: ${{ needs.setup.outputs.build_number }}<br><br>    steps:<br>      # Checkout the code<br>      - name: Checkout Source Code<br>        uses: actions/checkout@v4<br><br>      # Select Xcode version 16.1<br>      - name: Select Xcode Version<br>        run: |<br>          sudo xcode-select -s &quot;/Applications/Xcode_16.1.app&quot;<br>          xcodebuild -version<br><br>      # Setup provisioning profile and certificates for code signing<br>      - name: Configure Code Signing<br>        env:<br>          CERT_DATA: ${{ secrets.P12_CERTIFICATE }}<br>          CERT_PW: ${{ secrets.P12_PASSWORD }}<br>        run: |<br>          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles<br>          echo &quot;${{ secrets.PROVISIONING_PROFILE }}&quot; | base64 --decode &gt; ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision<br><br>          # Create and unlock a temporary keychain<br>          security create-keychain -p &quot;&quot; build.keychain<br>          security list-keychains -s build.keychain<br>          security default-keychain -s build.keychain<br>          security unlock-keychain -p &quot;&quot; build.keychain<br>          security set-keychain-settings<br><br>          # Decode and import the certificate<br>          echo $CERT_DATA | base64 --decode &gt; signingCertificate.p12<br>          security import signingCertificate.p12 -f pkcs12 -k build.keychain -P $CERT_PW -T /usr/bin/codesign<br>          security set-key-partition-list -S apple-tool:,apple: -s -k &quot;&quot; build.keychain<br><br>          rm signingCertificate.p12<br><br>      # Update Info.plist with the new build number<br>      - name: Update Build Number in Info.plist<br>        run: |<br>          /usr/libexec/PlistBuddy -c &quot;Set :CFBundleVersion $BUILD_NUMBER&quot; &quot;&lt;APP_NAME&gt;/Info.plist&quot;<br>          echo &quot;$BUILD_NUMBER&quot; &gt; &quot;build_number.txt&quot;<br><br>      # Extract target branch name from release event<br>      - name: Extract Target Branch<br>        id: extract_branch<br>        run: echo &quot;branch=${{ github.event.release.target_commitish }}&quot; &gt;&gt; $GITHUB_OUTPUT<br><br>      # Fetch all branches to ensure git operations succeed<br>      - name: Fetch All Branches<br>        run: git fetch --all<br><br>      # Commit updated build number and tag the release<br>      - name: Commit Build Number and Tag Release<br>        run: |<br>          BRANCH_NAME=&quot;${{ steps.extract_branch.outputs.branch }}&quot;<br>          git config user.name &quot;github-actions&quot;<br>          git config user.email &quot;github-actions@github.com&quot;<br>          git checkout &quot;$BRANCH_NAME&quot;<br>          git pull origin &quot;$BRANCH_NAME&quot;<br>          git pull origin &quot;$GITHUB_REF_NAME&quot;<br><br>          git add build_number.txt &lt;APP_NAME&gt;/Info.plist<br>          git commit -m &quot;🔢 Update build number to $BUILD_NUMBER&quot; || echo &quot;No changes to commit&quot;<br>          git push origin &quot;$BRANCH_NAME&quot;<br><br>          VERSION_NUMBER=$(/usr/libexec/PlistBuddy -c &quot;Print CFBundleShortVersionString&quot; &quot;&lt;APP_NAME&gt;/Info.plist&quot;)<br>          TAG_NAME=&quot;${{ github.event.release.tag_name }}&quot;<br>          git tag -f &quot;$TAG_NAME&quot;<br>          git push origin &quot;$TAG_NAME&quot; --force<br>          echo &quot;✅ Committed build number and tagged release v$VERSION_NUMBER.$BUILD_NUMBER&quot;<br><br>      # Clean and archive the app using xcodebuild<br>       name: Clean and Build the iOS App<br>        run: |<br>          xcodebuild clean \<br>            -workspace &lt;WORKSPACE_NAME&gt;.xcworkspace \<br>            -scheme &quot;${{ env.SCHEME_NAME }}&quot; \<br>            -configuration &quot;AdHoc&quot; \<br>            -sdk iphoneos \<br>            -quiet<br>            <br>          echo &quot;Clean success&quot;<br><br>          # Archive the app<br>          xcodebuild -scheme &quot;${{ env.SCHEME_NAME }}&quot; -workspace &lt;WORKSPACE_NAME&gt;.xcworkspace -sdk iphoneos -archivePath &lt;WORKSPACE_NAME&gt;.xcarchive clean archive PROVISIONING_PROFILE_SPECIFIER=&quot;${{ env.PROVISIONING_PROFILE_SPECIFIER_NAME}}&quot; CODE_SIGN_STYLE=&quot;Manual&quot;<br><br>          echo &quot;Archive success&quot;<br><br>      # Upload the IPA as an artifact for the deploy job<br>      - name: Upload IPA Artifact<br>        uses: actions/upload-artifact@v4<br>        with:<br>          name: PETRO-PASS<br>          path: build/${{ env.PRODUCT_NAME }}.ipa<br><br>      # Upload dSYM symbols for Crashlytics<br>      - name: Upload dSYM to Firebase Crashlytics<br>        run: |<br>          Pods/FirebaseCrashlytics/upload-symbols -gsp &lt;WORKSPACE_NAME&gt;/Firebase/${{ env.PLIST_FILE_NAME }} -p ios &lt;WORKSPACE_NAME&gt;.xcarchive/dSYMs/${{ env.PRODUCT_NAME }}.app.dSYM<br><br>  # ──────────────────────────────<br>  # Job 3: Deploy to TestFlight and Firebase<br>  # ──────────────────────────────<br>  deploy:<br>    name: Deploy to TestFlight and Firebase<br>    runs-on: macos-latest<br>    needs: build<br><br>    steps:<br>      # Checkout code<br>      - name: Checkout Source Code<br>        uses: actions/checkout@v4<br><br>      # Install Fastlane for iOS deployment<br>      - name: Install Fastlane<br>        run: sudo gem install fastlane -NV<br><br>      # Install Firebase CLI tools<br>      - name: Install Firebase CLI<br>        run: curl -sL https://firebase.tools | bash<br><br>      # Download IPA artifact from build job<br>      - name: Download IPA Artifact<br>        uses: actions/download-artifact@v4<br>        with:<br>          name: &lt;APP_NAME&gt;<br>          path: .<br><br>      # Setup Ruby Bundler and install dependencies (if any)<br>      - name: Setup Bundler and Install Dependencies<br>        run: |<br>          gem install bundler<br>          bundle install<br><br>      # Create App Store Connect API Key JSON file<br>      - name: Create App Store Connect API Key JSON<br>        run: |<br>          cat &lt;&lt;EOF &gt; fastlane/AuthKey.json<br>          {<br>            &quot;key_id&quot;: &quot;${{ secrets.PROD_KEY_ID }}&quot;,<br>            &quot;issuer_id&quot;: &quot;${{ secrets.PROD_ISSUER_ID }}&quot;,<br>            &quot;key&quot;: &quot;${{ secrets.PROD_KEY }}&quot;<br>          }<br>          EOF<br>          <br>      # Upload IPA to TestFlight with retry logic<br>      - name: Upload IPA to TestFlight<br>        env:<br>          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.PROD_KEY_ID }}<br>          APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.PROD_ISSUER_ID }}<br>          APP_STORE_CONNECT_API_KEY_PATH: fastlane/AuthKey.p8<br>        run: |<br>          for i in {1..3}; do<br>            echo &quot;🔁 Attempt $i to upload to TestFlight&quot;<br>            if bundle exec fastlane ios upload_only; then<br>              echo &quot;✅ Upload succeeded&quot;<br>              break<br>            elif [ &quot;$i&quot; -eq 3 ]; then<br>              echo &quot;❌ Upload failed after 3 attempts&quot;<br>              exit 1<br>            else<br>              echo &quot;⏳ Waiting before retry...&quot;<br>              sleep 30<br>            fi<br>          done<br><br>      # Publish app to Firebase App Distribution<br>      - name: Publish to Firebase App Distribution<br>        run: |<br>          firebase appdistribution:distribute &lt;APP_NAME&gt;.ipa \<br>            --app ${{ secrets.FIREBASE_APP_ID }} \<br>            --groups &quot;PROD&quot; \<br>            --release-notes &quot;${{ github.event.release.body }}&quot; \<br>            --token ${{ secrets.FIREBASE_TOKEN }}</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=89bfe8301144" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ iOS CI/CD with GitHub Actions: Firebase Deployment on Push Trigger (Part 1)]]></title>
            <link>https://medium.com/@vedantshirke/ios-ci-cd-with-github-actions-firebase-deployment-on-push-trigger-part-1-d85ba9d68bfe?source=rss-ffa5310e0b5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/d85ba9d68bfe</guid>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[mobile-devops]]></category>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[firebase]]></category>
            <dc:creator><![CDATA[Vedant Shirke]]></dc:creator>
            <pubDate>Thu, 10 Jul 2025 22:19:17 GMT</pubDate>
            <atom:updated>2025-07-10T22:19:17.080Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*emDwNLRmE6wlkdXyUbTbCg.png" /></figure><p>As an iOS developer, automating the build and distribution process saves time and reduces manual errors. In this blog series, I’ll walk you through a CI/CD pipeline using <strong>GitHub Actions</strong> for iOS apps.</p><p>In <strong>Part 1</strong>, we’ll focus on automatically building your app and distributing it to <strong>Firebase App Distribution</strong> when you <strong>push to a specific branch</strong> (like main, develop, or release/*).</p><p>In <strong>Part 2</strong>, we’ll expand this pipeline to trigger on GitHub Release publish events, dynamically generate build numbers, tag versions, and deploy to <strong>TestFlight</strong>.</p><h3>🧱 Pipeline Overview</h3><p><strong>Trigger:</strong> Push to branch (e.g., main)</p><p><strong>Steps:<br></strong>1. Checkout source code<br>2. Install dependencies<br>3. Configure code signing<br>4. Build and archive the iOS app<br>5. Upload to Firebase App Distribution</p><h3>⚙️ Job 1: Setup and Build on Push</h3><h3>Trigger</h3><pre>on:<br>  push:<br>    branches:<br>      - main</pre><h3>Steps</h3><p><strong>1. Checkout the code</strong></p><pre>- uses: actions/checkout@v4</pre><p><strong>2. Set up Ruby &amp; CocoaPods</strong></p><pre>- uses: ruby/setup-ruby@v1<br>  with:<br>    ruby-version: &#39;3.1.0&#39;<br>- run: |<br>    gem install cocoapods<br>    pod install</pre><p><strong>3. Configure Code Signing</strong></p><pre>mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles<br><br># Decode and import provisioning profile<br>echo &quot;${{ secrets.PROVISIONING_PROFILE }}&quot; | base64 --decode &gt; ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision<br><br># Set up keychain and import .p12<br>security create-keychain -p &quot;&quot; build.keychain<br>security list-keychains -s build.keychain<br>security default-keychain -s build.keychain<br>security unlock-keychain -p &quot;&quot; build.keychain<br><br># Decode and import certificate<br>echo $CERT_DATA | base64 --decode &gt; signingCertificate.p12<br>security import signingCertificate.p12 -f pkcs12 -k build.keychain -P $CERT_PW -T /usr/bin/codesign</pre><p><strong>4. Build and Archive the App</strong></p><pre>xcodebuild clean -workspace &lt;WORKSPACE_NAME&gt;.xcworkspace <br>  -scheme &quot;&lt;SCHEME_NAME&gt;&quot; <br>  -configuration &quot;Dist&quot; <br>  -sdk iphoneos <br>  -quiet<br><br>xcodebuild -scheme &quot;&lt;SCHEME_NAME&gt;&quot; \<br>  -workspace &lt;WORKSPACE_NAME&gt;.xcworkspace \<br>  -sdk iphoneos \<br>  -archivePath &lt;SCHEME_NAME&gt;.xcarchive \<br>  clean archive \<br>  PROVISIONING_PROFILE_SPECIFIER=&quot;&lt;PROFILE_NAME&gt;&quot; \<br>  CODE_SIGN_STYLE=&quot;Manual&quot; \<br>  CODE_SIGN_IDENTITY=&quot;Apple Distribution&quot;</pre><blockquote>Replace &lt;WORKSPACE_NAME&gt; and &lt;SCHEME_NAME&gt; with your app’s actual values.</blockquote><p><strong>5. Export IPA</strong></p><pre>xcodebuild -exportArchive \<br>  -archivePath &lt;SCHEME_NAME&gt;.xcarchive \<br>  -exportOptionsPlist plist/ExportOptions.plist \<br>  -exportPath build</pre><h3>☁️ Job 2: Upload to Firebase App Distribution</h3><p><strong>1. Install Firebase CLI</strong></p><pre>curl -sL https://firebase.tools | bash</pre><p><strong>2. Distribute the IPA</strong></p><pre>firebase appdistribution:distribute build/&lt;APP_NAME&gt;.ipa \<br>  --app ${{ secrets.FIREBASE_APP_ID }} \<br>  --groups &quot;&lt;GROUP_NAME&gt;&quot; \<br>  --release-notes &quot;${{ github.event.head_commit.message }} &quot; \<br>  --token ${{ secrets.FIREBASE_TOKEN }}</pre><blockquote>⚠️ Rename the generated .ipa to match your app name or update the path accordingly.</blockquote><p>This makes the latest .ipa available to your internal testers automatically after each push.</p><h3>🔐 Secrets Used</h3><p>P12_CERTIFICATE — Base64 of the signing certificate (.p12)<br>P12_PASSWORD — Password for .p12<br>PROVISIONING_PROFILE— Base64 of provisioning profile<br>FIREBASE_APP_ID— Firebase App ID<br>FIREBASE_TOKEN— Firebase CLI token</p><p>These are decoded and imported during the workflow.</p><h3>✅ What You Achieve</h3><p>1. 🔄 Automatic iOS app build on push<br>2. ✅ Code signing handled securely<br>3. ☁️ IPA uploaded to Firebase App Distribution<br>4. 🚫 No more manual builds and uploads!</p><h3>▶️ Coming Up in Part 2</h3><p>In the next post, we’ll cover:<br>1. Triggering builds on <strong>GitHub Release publish</strong><br>2. Automatically <strong>generating unique build numbers</strong><br>3. Tagging releases and committing back to the repo<br>4. Uploading to <strong>TestFlight</strong> with retries<br>5. Uploading dSYM to Crashlytics</p><p>Stay tuned — we’re just getting started with smarter iOS CI/CD.</p><p><em>By Vedant Shirke</em></p><pre>name: Build with Firebase CLI<br>on:<br>  push:<br>    branches:<br>      - &#39;develop&#39;<br>      - &#39;main&#39;<br>      - &#39;release/*&#39;<br><br>jobs:<br>  build:<br>    name: Build iOS app IPA<br>    runs-on: macos-latest<br>    steps:<br>      # Step 1: Checkout the code<br>      - name: Checkout<br>        uses: actions/checkout@v4<br>      <br>      # Step 2: Set up the Build environment<br>      - name: Setup Build Environment<br>        run: |<br>          # Set BUILD_ENV based on branch name<br>          echo &quot;BUILD_ENV=DEV&quot; &gt;&gt; $GITHUB_ENV<br>          echo &quot;FIREBASE_APP_ID_SECRET_NAME=FIREBASE_APP_ID_DEV&quot; &gt;&gt; $GITHUB_ENV<br>          echo &quot;PROVISIONING_PROFILE_NAME=PROVISIONING_PROFILE&quot; &gt;&gt; $GITHUB_ENV<br>          echo &quot;PROVISIONING_PROFILE_SPECIFIER_NAME=&lt;PROFILE_NAME&gt;&quot; &gt;&gt; $GITHUB_ENV<br>          echo &quot;SCHEME_NAME=&lt;SCHEME_NAME&gt;&quot; &gt;&gt; $GITHUB_ENV<br>          echo &quot;PRODUCT_NAME=PETRO-PASS\ DEV&quot; &gt;&gt; $GITHUB_ENV<br>          echo &quot;PLIST_FILE_NAME=GoogleService-Info-dev.plist&quot; &gt;&gt; $GITHUB_ENV<br>          <br>      - name: Verify BUILD_ENV<br>        run: |<br>          echo &quot;Current BUILD_ENV: ${{ env.BUILD_ENV }}&quot;<br>          echo &quot;Firebase App Id: ${{ secrets[env.FIREBASE_APP_ID_SECRET_NAME] }}&quot;<br>          echo &quot;Provisioning Profile: ${{ secrets[env.PROVISIONING_PROFILE_NAME] }}&quot;<br>          echo &quot;Provisioning Profile Specifier: ${{ env.PROVISIONING_PROFILE_SPECIFIER_NAME}}&quot;<br>          echo &quot;Scheme Name: ${{ env.SCHEME_NAME }}&quot;<br>          echo &quot;Product Name: ${{ env.PRODUCT_NAME }}&quot;<br>          echo &quot;Plist File Name: ${{ env.PLIST_FILE_NAME }}&quot;<br>          <br>      #Step 3: Set up the environment (CocoaPods, etc)<br>      - name: Set up Ruby for CocoaPods<br>        uses: ruby/setup-ruby@v1<br>        with:<br>          ruby-version: &#39;3.1.0&#39;<br>          <br>      - name: Get xcodebuild version<br>        run: |<br>          sudo xcode-select -s &quot;/Applications/Xcode_16.1.app&quot;<br>          xcodebuild -version<br>          <br>      - name: Install CocoaPods Dependencies<br>        run: |<br>          gem install cocoapods<br>          pod install<br>          <br>      #Step 4: Set up Provisioning Profile and Certificates<br>      - name: Set up Provisioning Profile and Certificates<br>        env:<br>          CERT_DATA: ${{ secrets.P12_CERTIFICATE }}<br>          CERT_PW: ${{ secrets.P12_PASSWORD }}<br>        run: |<br><br>          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles<br>          echo &quot;${{ secrets[env.PROVISIONING_PROFILE_NAME] }}&quot; | base64 --decode &gt; ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision<br>          <br>          # Create and unlock keychain<br>          security create-keychain -p &quot;&quot; build.keychain<br>          security list-keychains -s build.keychain<br>          security default-keychain -s build.keychain<br>          security unlock-keychain -p &quot;&quot; build.keychain<br>          security set-keychain-settings<br>          <br>          echo $CERT_DATA | base64 --decode &gt; signingCertificate.p12<br>          echo &quot;certificate decoded&quot;<br>          security import signingCertificate.p12 -f pkcs12 -k build.keychain -P $CERT_PW -T /usr/bin/codesign<br>          security set-key-partition-list -S apple-tool:,apple: -s -k &quot;&quot; build.keychain<br>          <br>          file signingCertificate.p12<br>          <br>          rm signingCertificate.p12<br>          <br>      #Step 5: Clean and Build the iOS App<br>      - name: Clean and Build the iOS App<br>        run: |<br>          xcodebuild clean \<br>            -workspace &lt;WORKSPACE_NAME&gt;.xcworkspace \<br>            -scheme &quot;${{ env.SCHEME_NAME }}&quot; \<br>            -configuration &quot;AdHoc&quot; \<br>            -sdk iphoneos \<br>            -quiet<br>            <br>          echo &quot;Clean success&quot;<br><br>          # Archive the app<br>          xcodebuild -scheme &quot;${{ env.SCHEME_NAME }}&quot; -workspace &lt;WORKSPACE_NAME&gt;.xcworkspace -sdk iphoneos -archivePath &lt;WORKSPACE_NAME&gt;.xcarchive clean archive PROVISIONING_PROFILE_SPECIFIER=&quot;${{ env.PROVISIONING_PROFILE_SPECIFIER_NAME}}&quot; CODE_SIGN_STYLE=&quot;Manual&quot;<br><br>          echo &quot;Archive success&quot;<br>          <br>      - name: Upload dSYM<br>        run: |<br>          ls Pods/FirebaseCrashlytics/<br>          ls Pods/FirebaseCrashlytics <br>          Pods/FirebaseCrashlytics/upload-symbols -gsp &lt;WORKSPACE_NAME&gt;/Firebase/${{ env.PLIST_FILE_NAME }} -p ios &lt;WORKSPACE_NAME&gt;.xcarchive/dSYMs/${{ env.PRODUCT_NAME }}.app.dSYM<br>            <br>      - name: Export the IPA<br>        run: |<br>          xcodebuild -exportArchive \<br>            -archivePath &lt;WORKSPACE_NAME&gt;.xcarchive \<br>            -exportOptionsPlist plist/ExportOptions.plist \<br>            -exportPath build<br>            <br>          ls build<br>          <br>      - name: Install Firebase Tools<br>        run: |<br>          curl -sL https://firebase.tools | bash<br><br>      - name: Get Firebase Tools Version<br>        run: |<br>          firebase --version<br><br>        ## For dev should be update to APK<br>      - name: Publish App to Firebase App Distribution<br>        run: |<br>          firebase appdistribution:distribute build/${{ env.PRODUCT_NAME }}.ipa \<br>            --app ${{ secrets[env.FIREBASE_APP_ID_SECRET_NAME] }} \<br>            --groups &quot;${{ env.BUILD_ENV }}&quot; \<br>            --release-notes &quot;${{ github.event.head_commit.message }}&quot; \<br>            --token ${{ secrets.FIREBASE_TOKEN }}</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d85ba9d68bfe" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>