<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.searle.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.searle.dev/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-04-08T13:16:02+00:00</updated><id>https://www.searle.dev/feed.xml</id><title type="html">Tim Searle</title><subtitle>A personal journal sharing my opinions on the technology industry, my project statuses and learnings from my journey as a software engineer.</subtitle><entry xml:lang="en"><title type="html">Do you trust your assistant?</title><link href="https://www.searle.dev/2026/02/15/agentic-identity.html" rel="alternate" type="text/html" title="Do you trust your assistant?" /><published>2026-02-15T00:00:00+00:00</published><updated>2026-02-15T00:00:00+00:00</updated><id>https://www.searle.dev/2026/02/15/agentic-identity</id><content type="html" xml:base="https://www.searle.dev/2026/02/15/agentic-identity.html"><![CDATA[<p>Personal assistants used to be the preserve of the wealthy, and special tooling was not needed for them to perform sensitive actions - trust levels were higher. Soon hundreds of millions of people will have them, and the entire identity and access model for these services needs to be upgraded.</p>

<p>I recently deployed the popular AI assistant, <a href="https://openclaw.ai">OpenClaw</a>, myself for some fun experimentation. Yes, it’s been littered with vulnerabilities, there are security researchers all over it, and there’s been a lot of sensationalist hype online - but it is an important piece of software.</p>

<p>It’s a glimpse into the direction we’re heading toward - mainstream AI personal assistants that can dynamically achieve <em>anything</em>, and perhaps the <a href="https://searle.dev/2026/02/14/death-of-apps.html">”death of apps”</a>.</p>

<h2 id="configuring-openclaw">Configuring OpenClaw</h2>

<p>Although I was terrified by the coverage of the security issues, I just <em>had</em> to run this thing and form my own opinions. I actually ended up configuring my OpenClaw instance so aggressively constrained it was <em>almost</em> useless.</p>

<ul>
  <li>Dedicated Raspberry Pi 4 Model B</li>
  <li>Inbound connectivity only over SSH</li>
  <li>A dedicated Linux user for OpenClaw with no interactive login, no SSH, no sudo, and read/write access restricted to specific directories</li>
  <li>Docker requires sudo that the dedicated user cannot obtain; OpenClaw runs as root inside the container, but user namespace remapping ensures a container escape would land as the powerless dedicated user, not root</li>
  <li>Strict outbound port filtering with <a href="https://wiki.ubuntu.com/UncomplicatedFirewall">ufw</a> (only DNS, HTTP/S, and NTP allowed)</li>
  <li>No access to any accounts, browser sessions and secrets.</li>
</ul>

<p>Initially, I was going to go further, and begin allow-listing every outbound hostname that OpenClaw wanted to interact with, and extensively analysed the outbound <code class="language-plaintext highlighter-rouge">ufw</code> logs, but realised this would just not be valuable. None of this protects against OpenClaw connecting to some allow-listed host, and pumping out data, keys and credentials, due to a prompt injection or compromised skill or tool.</p>

<p>I was still hesitant to give it access to tools, to systems, worried about the <a href="https://x.com/theonejvo/status/2016510190464675980">one-click exploits</a> that had been flagged.</p>

<p>But for OpenClaw’s utility to be maximised, it needs to have a large amount of access granted to some of the most sensitive credentials and services its owner possesses.</p>

<h2 id="so-can-we-trust-them">So, can we trust them?</h2>

<p>Personally, I keep a mental separation between OpenClaw’s security vulnerabilities and the general security model of AI assistants. Yes, there are vulnerabilities and <a href="https://github.com/openclaw/openclaw/security/advisories">plenty of them</a>, but these are generally coding, control flow and logic problems. These will be solved by researchers, by SAST, DAST and SCA tools, and as the models improve, the issues will occur less frequently - but none of these mitigations change the part of the threat model I am most concerned with in the current generation of AI assistants we’re using.</p>

<p>AI assistants currently operate through having some ability to act on your behalf, and invariably, they do this by having direct access to your long-lived credentials, your tokens, your API keys, your browser sessions, your email, and more.</p>

<p>In a world of prompt injection, malicious skills, and deep supply chain attacks targeting software like OpenClaw, these high-value items become the real goals, enabling account takeovers and data exfiltration.</p>

<h2 id="the-problem">The problem</h2>

<p>So, how can we apply some basic identity concepts, not just for OpenClaw, but for any AI personal assistants, and what would a more secure architecture look like?</p>

<p>We can split this problem into two areas, and in the Identity space, that is always going to be authentication and authorisation.</p>

<h3 id="authentication">Authentication</h3>

<p>When we talk about authentication, we always mean “who are you?”, and in the context of AI assistants we currently see two common patterns:</p>

<ul>
  <li>They impersonate their owner</li>
  <li>They have their own dedicated identity (delegation)</li>
</ul>

<p>Starting with impersonation, why is this an issue? The assistant is just acting as <strong>me</strong>. It’s indistinguishable from me interacting with these services. This means it can perform any task, with no approvals, and most importantly, in response to a prompt feedback loop.</p>

<p>Some people have attempted to solve this with the second pattern. They give their assistant its own accounts, with dedicated credentials, to segregate the identity from their own.  This is often referred to as “Delegation”.</p>

<p><a href="https://1password.com/blog/its-openclaw">1Password have written an interesting article</a> where they’re committing to solve this problem by adding broader support for these agentic identities.</p>

<p>1Password allows for more guarded access to credentials, it stops them being stored on disk in plain text, and you could even enforce interactive access to those credentials. Ostensibly, this gives you some level of protection but it then <em>limits</em> what an AI assistant is able to do on your behalf.</p>

<p>Without a solid authorisation model in addition to the second identity, it can’t handle my emails, or my appointments if it does not have ongoing, secure, access to them.</p>

<p>Forwarding emails to it is not great, it’s not the autonomous and proactive AI assistant we all want.</p>

<p>I don’t think these two patterns are mutually exclusive. So, how can they both interact? And most importantly, what are they allowed to do?</p>

<h3 id="authorisation">Authorisation</h3>

<p>In the Identity space, when we talk about “what can you do?”, we’re talking about authorisation.</p>

<p>In the above section we talked about impersonation, and a segregation of duties with a specialised agentic identity. In both scenarios, how can we gate what the assistant can do?</p>

<p>We could use markdown files, we could use system prompts, we could use a permissions model around tool calls (similar to that of Claude Code, Codex and Copilot CLI). We could even wait for the models to improve their prompt injection resilience and intrinsically trust them more - but these are all only operating “locally”, at a pre-API call phase, and do not solve the root issue that the agent must have access to the raw credentials in order to be able to perform the tasks it is trying to do.</p>

<p>Once an AI assistant decides to make a tool call, and it has the credentials, that’s the end of the road for the security model - it already has the ability to perform its task, and potentially cause damage - we need authorisation policies defined both within the assistant itself and also on the third-party services.</p>

<h2 id="an-ideal-flow">An ideal flow</h2>

<p>So what do I actually want? Let’s use a high-privileged, sensitive journey, with a clear real-world analogy.</p>

<p>I’m emailed an invoice, and I want my assistant to pay it on my behalf. This requires:</p>

<ul>
  <li>Access to my mailbox</li>
  <li>Access to a payment service</li>
</ul>

<p>Would I want my assistant to automatically pay this? Of course not. In the same way if I had a human assistant I would not want them to immediately pay an invoice just because it landed in my inbox.</p>

<p>So what controls do we need?</p>

<ul>
  <li>Assistant needs its own user/machine identity.</li>
  <li>Assistant needs read-access to <strong>my</strong> emails. E.g., perhaps IMAP-only, but ideally these scopes are handled at the email service layer, not just the protocol. Think how a personal assistant has access to their client’s emails today.</li>
  <li>The assistant needs a policy that determines whether they are allowed to perform the privileged/sensitive action, such as paying invoices</li>
  <li>If the policy is met, the assistant needs to be able to obtain authorisation <strong>from me</strong> to perform the action</li>
  <li>The payment service must verify that the assistant making the request is authorised to act on the behalf of the account principal (me)</li>
</ul>

<p>To solve this, we need improvements to both the AI assistant’s permissions/policy model, but primarily, the services we’re interacting with need to provide some “AI Assistant-native” authorisation capabilities.</p>

<h2 id="ai-assistant-native-integrations">AI Assistant Native Integrations</h2>

<p>What we’re talking about requires third-parties to plan, design and build for these use cases. And we need this fast. Their identity and access management model needs to natively provide the ability to delegate access to your virtual assistant, and tightly control and audit what it can do.</p>

<p>If companies do not provide these solutions, people will find workarounds, and these workarounds will have a worse risk-posture.</p>

<h3 id="oauth-and-openid-connect">OAuth and OpenID Connect</h3>

<p>I want to be really clear, we are not innovating new ground here - this journey is well-supported by the <a href="https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html">OpenID Connect Client-Initiated Backchannel Authentication (CIBA)</a> flow, and the <a href="https://datatracker.ietf.org/doc/html/rfc8693">RFC 8693 - OAuth 2.0 Token Exchange</a> specification.</p>

<p><strong>We do not need to create new standards, what we need is greater adoption.</strong></p>

<p>CIBA allows an AI assistant to create some sort of “intent” that it wants to act on, the assistant can then send that intent to the authorisation server. The authorisation server can then send me a request to authenticate via an out-of-band channel - think push notification - for me to approve the request.</p>

<p>Once the token is issued, the AI assistant’s personal identity, and my authorisation, can be exchanged for the new token.</p>

<p>OAuth 2.0 Token Exchange results in an access token that is a composite identity of two participants:</p>

<ul>
  <li>Actor (<code class="language-plaintext highlighter-rouge">act</code>): AI Assistant</li>
  <li>Subject (<code class="language-plaintext highlighter-rouge">sub</code>): User</li>
</ul>

<p>So we need these services to not only allow us to create more fine-grained access to their respective services via their APIs, we also need them to understand that we are authorising additional actors on our accounts that can have their own permissions around highly privileged actions.</p>

<p>So, the specs are written, the technology exists, what’s stopping us here?</p>

<h2 id="what-needs-to-happen">What needs to happen?</h2>

<p>Service providers need to transition from the expectation that one account equals one identity - “everyone” will be using these assistants and we need to be able to allow delegated access for the assistant to act on our behalf within our banking, email, travel planning, shopping systems and more.</p>

<p>The big tech companies who build our phones and our social media need to treat AI assistants as a first-class identity that can interact with our accounts.</p>

<p>The whole system needs an overhaul, because people aren’t going to be running these assistants on their Raspberry Pis or Mac Minis. They aren’t going to be threat-modelling their setup. They want magic.</p>

<p>This isn’t going to be optional. This is the same journey banks went through with the Open Banking push. The banks that got there first adopting the FinTech regulations are the ones that are winning today. The ones that didn’t are being screen-scraped by spurious software and are putting their customers and their data at risk.</p>

<p>Users need simple, low-friction approval flows, that fit into their day-to-day life. Think push notifications, not config files.</p>

<p>What happens if services don’t do this?</p>

<p>20 years ago, we made a transition, no matter how small your business is, you need to have a website or be using an online service for customer acquisition, otherwise you will struggle and your service will suffer.</p>

<p>Now? If an AI assistant can’t securely interact with, or see, your service - you <strong>will</strong> struggle and your service <strong>will</strong> suffer.</p>]]></content><author><name>Tim Searle</name></author><category term="ai" /><category term="oauth" /><category term="identity" /><summary type="html"><![CDATA[Personal assistants used to be the preserve of the wealthy, and special tooling was not needed for them to perform sensitive actions - trust levels were higher. Soon hundreds of millions of people will have them, and the entire identity and access model for these services needs to be upgraded.]]></summary></entry><entry xml:lang="en"><title type="html">“The death of apps”</title><link href="https://www.searle.dev/2026/02/14/death-of-apps.html" rel="alternate" type="text/html" title="“The death of apps”" /><published>2026-02-14T00:00:00+00:00</published><updated>2026-02-14T00:00:00+00:00</updated><id>https://www.searle.dev/2026/02/14/death-of-apps</id><content type="html" xml:base="https://www.searle.dev/2026/02/14/death-of-apps.html"><![CDATA[<p>Back in 2016, I was interviewed at WWDC about the latest iOS release, and my comments on Siri App integrations were listed under a heading - the <a href="https://www.campaignlive.co.uk/article/apple-breaks-down-walled-garden-opening-siri-maps-developers/1398495#:~:text=The%20death%20of%20apps&amp;text=Citing%20specific%20Apple%20iOS%20features,no%20longer%20by%20specific%20downloads.%22">“death of apps”</a>.</p>

<p>Despite the interviewer taking slight journalistic liberties with my thoughts, I do think my general intent behind the words was actually captured quite well.</p>

<p>In a recent <a href="https://lexfridman.com/peter-steinberger-transcript#chapter18_ai_agents_will_replace_80_of_apps">interview with Peter Steinberger</a>, the creator of AI assistant, <a href="https://openclaw.ai">OpenClaw</a>, he stated that 80% of apps will disappear because AI assistant will be able to handle the majority of features that users expect of their app. This statement was a real déjà vu moment, reminding me of my own comments back in 2016.</p>

<p>Did my prediction come true? No. Siri still can’t do it. But OpenClaw can.</p>

<p>Do I think an open-source tool will go mainstream and fulfil this goal? Probably not. It’s an incredible piece of software, and has created an entire new industry category. But in the same way decentralised crypto didn’t take off until companies centralised it and <em>made it</em> adoptable, I don’t think this is <em>it</em>. But will the big tech incumbents land something that does tap in to the average user and provide a launch pad for automations into their current apps? Almost certainly.</p>]]></content><author><name>Tim Searle</name></author><category term="ai" /><category term="apps" /><category term="mobile" /><summary type="html"><![CDATA[Back in 2016, I was interviewed at WWDC about the latest iOS release, and my comments on Siri App integrations were listed under a heading - the “death of apps”.]]></summary></entry><entry xml:lang="en"><title type="html">Selling shovels</title><link href="https://www.searle.dev/2026/01/30/gold-rush.html" rel="alternate" type="text/html" title="Selling shovels" /><published>2026-01-30T00:00:00+00:00</published><updated>2026-01-30T00:00:00+00:00</updated><id>https://www.searle.dev/2026/01/30/gold-rush</id><content type="html" xml:base="https://www.searle.dev/2026/01/30/gold-rush.html"><![CDATA[<p>You’ve no doubt seen the hype about <a href="https://openclaw.ai">OpenClaw</a>, formerly known as Moltbot, formerly known as Clawdbot.</p>

<p>But have you seen <a href="https://setupclaw.com">SetupClaw</a>?</p>

<p>Well, as the proverb says, “in a <a href="https://en.wikipedia.org/wiki/California_gold_rush">gold rush</a>, you should sell shovels”.</p>]]></content><author><name>Tim Searle</name></author><category term="ai" /><summary type="html"><![CDATA[You’ve no doubt seen the hype about OpenClaw, formerly known as Moltbot, formerly known as Clawdbot.]]></summary></entry><entry xml:lang="en"><title type="html">Embracing imperfection with commonplace books</title><link href="https://www.searle.dev/2026/01/29/commonplace-books.html" rel="alternate" type="text/html" title="Embracing imperfection with commonplace books" /><published>2026-01-29T00:00:00+00:00</published><updated>2026-01-29T00:00:00+00:00</updated><id>https://www.searle.dev/2026/01/29/commonplace-books</id><content type="html" xml:base="https://www.searle.dev/2026/01/29/commonplace-books.html"><![CDATA[<p>I’ve always been someone that has been a bit of a perfectionist and over thought my own processes for achieving goals. It’s made it hard to ship blog posts regularly, land my side projects and generally complete tasks.</p>

<p>For me, one part of creating things involves having a place to dump ideas down quickly that isn’t digital - there’s something about taking the effort to note things down by hand and create a more mindful moment, allowing me to take a pause and really process what I’m thinking about.</p>

<p>However, I’d buy a brand new notebook and I’d painstakingly fuss over what I was going to use that notebook for, what structure I’d choose to write in it with, and if I made a mistake while writing in it, I’d be incredibly frustrated that now that page/part was “ruined”. The very process of just writing notes would suddenly throw up roadblocks in front of just delivering the creative ideas I’d just had.</p>

<p>It came from a place of wanting to do great work, but ultimately it crushed my creativity, halted my momentum and kept me focused on things that didn’t drive the goals I was aiming for.</p>

<p>Recently, while telling him about my new notebook purhcase, my friend <a href="https://jordanterry.co.uk/about/">Jordan Terry</a> introduced me to the term “<a href="https://en.wikipedia.org/wiki/Commonplace_book">Commonplace book</a>”, and as soon as I read more about it, I knew that’s exactly how I needed to treat my notebooks going forwards. They just need to be a stream of consciousness, interesting ideas, quotes, lists, and random thoughts. It didn’t matter if there was a mistake, if what was on one page was unrelated to the next, or if another page was a shopping list. It feels strange, almost like I <em>needed</em> validation that there’s a concept of an unstructured notebook, but I’ll take what I can get.</p>

<p>So, why am I writing this in a blog? Well, after a recent conversation with <a href="https://ben-phillips.net">Ben Phillips</a>, who’s re-embracing blogging and building again, he said that he was “going to write for me” and just “see if anyone reads it”.</p>

<p>That statement, plus the discovery of this concept, it just made me want to get something out there and stop worrying about it so much going forwards, so here we are, hopefully I’ll get more than nine blog posts out there over the next ten years.</p>

<p><em>And yes - I did buy a new notebook and pen because nothing beats that feeling of starting a new notebook.</em></p>]]></content><author><name>Tim Searle</name></author><category term="notes" /><category term="productivity" /><summary type="html"><![CDATA[I’ve always been someone that has been a bit of a perfectionist and over thought my own processes for achieving goals. It’s made it hard to ship blog posts regularly, land my side projects and generally complete tasks.]]></summary></entry><entry xml:lang="en"><title type="html">swift-dependency-graph</title><link href="https://www.searle.dev/2025/12/13/swift-dependency-graph.html" rel="alternate" type="text/html" title="swift-dependency-graph" /><published>2025-12-13T00:00:00+00:00</published><updated>2025-12-13T00:00:00+00:00</updated><id>https://www.searle.dev/2025/12/13/swift-dependency-graph</id><content type="html" xml:base="https://www.searle.dev/2025/12/13/swift-dependency-graph.html"><![CDATA[<p>I’ve just open sourced a small tool called <a href="https://github.com/timsearle/swift-dependency-graph"><code class="language-plaintext highlighter-rouge">swift-dependency-graph</code></a>. This was coded completely via <a href="https://github.com/features/copilot/cli">copilot-cli</a>, including publishing this blog post!</p>

<p>Given the root of an iOS app, it builds a dependency graph across Xcode targets and SwiftPM packages, and can export it in a few formats (including an interactive HTML graph).</p>

<p>If you want to try it out:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/timsearle/swift-dependency-graph
<span class="nb">cd </span>swift-dependency-graph
swift run swift-dependency-graph <span class="nt">--help</span>
</code></pre></div></div>

<p>It’s still early, and I’m mostly interested in correctness and good fixtures — if you run it on a real project and spot gaps, I’d love an issue (or a PR).</p>]]></content><author><name>Tim Searle</name></author><category term="swift" /><category term="ios" /><category term="xcode" /><category term="tooling" /><summary type="html"><![CDATA[I’ve just open sourced a small tool called swift-dependency-graph. This was coded completely via copilot-cli, including publishing this blog post!]]></summary></entry><entry xml:lang="en"><title type="html">Demystifying Git</title><link href="https://www.searle.dev/2025/08/31/demystifying-git.html" rel="alternate" type="text/html" title="Demystifying Git" /><published>2025-08-31T00:00:00+00:00</published><updated>2025-08-31T00:00:00+00:00</updated><id>https://www.searle.dev/2025/08/31/demystifying-git</id><content type="html" xml:base="https://www.searle.dev/2025/08/31/demystifying-git.html"><![CDATA[<p>This time last year, I gave a presentation internally at M&amp;S, for our engineering community, called “Demystifying Git.” Rather than the usual how-to guide, I wanted people to walk away with a clearer picture of how Git actually works — and how that understanding can make it easier to use and to debug the awkward states repositories sometimes get into.</p>

<p>The session went down well, but I recently realised the slides have just been sitting idle on my laptop ever since. So, here they are! Take a look, and let me know if you learned something new - or if you’ve got something to add or correct.</p>]]></content><author><name>Tim Searle</name></author><category term="git" /><category term="slides" /><summary type="html"><![CDATA[This time last year, I gave a presentation internally at M&amp;S, for our engineering community, called “Demystifying Git.” Rather than the usual how-to guide, I wanted people to walk away with a clearer picture of how Git actually works — and how that understanding can make it easier to use and to debug the awkward states repositories sometimes get into.]]></summary></entry><entry xml:lang="en"><title type="html">A Custom GPT for OAuth &amp;amp; OIDC</title><link href="https://www.searle.dev/2025/03/21/oauth-rfc-gpt.html" rel="alternate" type="text/html" title="A Custom GPT for OAuth &amp;amp; OIDC" /><published>2025-03-21T00:00:00+00:00</published><updated>2025-03-21T00:00:00+00:00</updated><id>https://www.searle.dev/2025/03/21/oauth-rfc-gpt</id><content type="html" xml:base="https://www.searle.dev/2025/03/21/oauth-rfc-gpt.html"><![CDATA[<p>If you’ve ever felt lost navigating OAuth specs, or any IETF RFCs for that matter, you’re not alone. For the last few years, I’ve been working on an identity migration project that has been heavily tied to OAuth and OpenID Connect. Ensuring that our system and its integrations comply with, or extend, the OAuth and OpenID Connect standards has been one of our key north stars and informs almost all of our design decisions.</p>

<p>There are over 25 individual specifications, covering everything from core OAuth 2.0 and Bearer Tokens to extensions like Token Exchange and JSON Web Tokens, and these documents are owned and maintained by multiple organisations - the IETF and the OpenID Foundation. These specifications are incredibly detailed and can be difficult to understand, particularly for those who are new to the standards. A lot of these RFCs are actually extensions of the original specifications, and it can be difficult to actually locate the specification most aligned to your requirement/need.</p>

<p>For example, a defined token revocation mechanism is not mentioned at all in the OAuth 2.0 Authorization Framework <a href="https://datatracker.ietf.org/doc/html/rfc6749">RFC 6749</a>, but is instead defined within an extension of that specification, OAuth 2.0 Token Revocation <a href="https://datatracker.ietf.org/doc/html/rfc7009">RFC 7009</a>.</p>

<p>I needed a better way to navigate all of this.</p>

<p>Enter <a href="https://chatgpt.com">ChatGPT</a>.</p>

<p>It was clear to me that ChatGPT had been trained on the full IETF archive of RFCs. These documents are plain text (ASCII) and follow a well-defined and consistent structure, which plays to an LLM’s strengths in parsing and retrieving text-based information.</p>

<p>However, finding the right spec was becoming a time sink. After weeks of juggling ChatGPT answers and subsequent Google searches, I found myself continually having to refine my queries. ChatGPT often gave plausible-sounding replies that were inaccurate, which meant I had to double-check everything against multiple RFCs anyway. Sigh.</p>

<p>I found myself continually needing to craft the appropriate prompt, asking for specificity and accuracy to be prioritised over “helpfulness” and to ensure that it always cited the relevant IETF RFC number and section.</p>

<p>This constant back and forth and repetitive re-prompting led me to decide to try the <a href="https://openai.com/index/introducing-gpts/">OpenAI custom GPT functionality</a>.</p>

<h2 id="introducing-the-oauth-rfc-assistant">Introducing the OAuth RFC Assistant</h2>

<p>My focus when creating the custom GPT was quite simple, I needed to ensure the following requirements:</p>

<ul>
  <li>Always cite the RFC number and section.</li>
  <li>Never make assumptions or extrapolate beyond what the RFC states.</li>
</ul>

<p><img src="/assets/images/oauth-assistant.png" alt="OAuth RFC Assistant" /></p>

<p>After several months of using the assistant and tweaking the prompts - I’m really pleased to share the <a href="https://chatgpt.com/g/g-6731ea7a6678819082e7127e495a3b3a-oauth-rfc-assistant">OAuth RFC Assistant</a> for others to use!</p>

<p>And I love it.</p>

<p>Members of my team and other developers in the identity space have also found it really useful. It’s accelerated our decision-making and made the standards much more digestible.</p>

<h2 id="prompting-takeaways">Prompting Takeaways</h2>

<p>Here are a few lessons learned while crafting the appropriate prompts to drive the behaviour of the custom GPT:</p>

<ul>
  <li>Test how it handles incorrect or trick questions - ask it leading questions you know to be incorrect to ensure it doesn’t make assumptions. Effectively - validate your error cases! We all know how easy it is to lead the witness with ChatGPT.</li>
</ul>

<blockquote>
  <p><strong>Me:</strong> Why does every grant type always require a client secret?</p>

  <p><strong>OAuth RFC Assistant:</strong> Not every OAuth 2.0 grant type requires a client secret—this depends on the type of client and the grant type being used. The requirement for a client secret is governed by RFC 6749 (OAuth 2.0) and is primarily determined by whether the client is classified as confidential or public.</p>
</blockquote>

<ul>
  <li>Give it clear constraints it must operate within - always cite the RFC number and section. This forces the GPT to derive an answer with a clear structure which minimises (but does not remove) hallucinations. This is very helpful for when you have a rough idea of a concept, but no idea what the formal RFC is.</li>
</ul>

<p><img src="/assets/images/on-behalf.png" alt="On-behalf-of" /></p>

<ul>
  <li>Tell it specifically what you want it to do when it doesn’t know the answer.</li>
</ul>

<blockquote>
  <p><strong>Prompt</strong>: If a detail is not mentioned in an RFC or OIDC specification, the assistant will explicitly state that the information is unavailable, unspecified, or unknown.</p>
</blockquote>

<ul>
  <li>Give hints about the format you expect in the answer.</li>
</ul>

<blockquote>
  <p><strong>Prompt</strong>: It highlights compliance levels (e.g., MUST, SHOULD) as defined in the documents without ambiguous interpretations.</p>
</blockquote>

<h2 id="summary">Summary</h2>

<p>This <strong>tool</strong> has been a real accelerator for my team - but it’s so important to remember that this is all it is. It’s a tool. I still always use its answers to expedite a dive into the appropriate RFC and section to validate the information. It’s a great way to get a quick answer and to navigate hundreds of documents (and thousands of lines of standards), but it’s not a replacement for building out subject-matter expertise that comes from engaging with the RFCs directly.</p>

<p>Please give the <a href="https://chatgpt.com/g/g-6731ea7a6678819082e7127e495a3b3a-oauth-rfc-assistant">OAuth RFC Assistant</a> a try, and let me know how you get on and if you have any feedback!</p>]]></content><author><name>Tim Searle</name></author><category term="oauth" /><category term="identity" /><category term="chatgpt" /><summary type="html"><![CDATA[If you’ve ever felt lost navigating OAuth specs, or any IETF RFCs for that matter, you’re not alone. For the last few years, I’ve been working on an identity migration project that has been heavily tied to OAuth and OpenID Connect. Ensuring that our system and its integrations comply with, or extend, the OAuth and OpenID Connect standards has been one of our key north stars and informs almost all of our design decisions.]]></summary></entry><entry xml:lang="en"><title type="html">Accessing private GitHub repositories within GitHub Actions</title><link href="https://www.searle.dev/2024/02/22/github-actions-tokens.html" rel="alternate" type="text/html" title="Accessing private GitHub repositories within GitHub Actions" /><published>2024-02-22T00:00:00+00:00</published><updated>2024-02-22T00:00:00+00:00</updated><id>https://www.searle.dev/2024/02/22/github-actions-tokens</id><content type="html" xml:base="https://www.searle.dev/2024/02/22/github-actions-tokens.html"><![CDATA[<p>Generally, <a href="https://docs.github.com/en/actions">GitHub Actions</a> is incredibly simple when interacting with private repositories, either through the <code class="language-plaintext highlighter-rouge">GITHUB_TOKEN</code> or through creation a Personal Access Token. However, when the tools that your GitHub Action interacts with require to authenticate, injecting that personal access token can become challenging.</p>

<p>Here I show two easy ways to enable your GitHub Actions to interact with private repositories for:</p>

<ul>
  <li><a href="https://bundler.io">Bundler</a></li>
  <li><a href="https://www.swift.org/documentation/package-manager/">Swift Package Manager</a></li>
</ul>

<p>In both of these examples, it’s incredibly important to use GitHub Action’s secrets to store the personal access token, and then inject that token into the environment of the job that requires it. This ensures it is redacted in any runner logs and not visible in plain text.</p>

<h2 id="cloning-private-repositories-through-ruby">Cloning Private Repositories through Ruby</h2>

<p>There’s an obscure variable that Bundler uses to authenticate with GitHub, <code class="language-plaintext highlighter-rouge">BUNDLE_GITHUB__COM</code>:</p>

<p>Specify the variable <code class="language-plaintext highlighter-rouge">BUNDLE_GITHUB__COM: x-access-token:${{ secrets.$SOME_PAT }}</code> in your <code class="language-plaintext highlighter-rouge">job</code>’s env` key and ensure the URL to clone the ruby gem is using https.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'private-dependency'</span><span class="p">,</span> <span class="ss">git: </span><span class="s1">'https://github.com/your-org/private-repo'</span>
</code></pre></div></div>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Deploy"</span>  
  
<span class="na">on</span><span class="pi">:</span>  
  <span class="na">workflow_dispatch</span><span class="pi">:</span>  
  
<span class="na">jobs</span><span class="pi">:</span>  
  <span class="na">deploy-beta</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Deploy</span><span class="nv"> </span><span class="s">Beta"</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">timeout-minutes</span><span class="pi">:</span> <span class="m">10</span>
    <span class="na">env</span><span class="pi">:</span>  
      <span class="na">BUNDLE_GITHUB__COM</span><span class="pi">:</span> <span class="s">x-access-token:${{ secrets.PRIVATE_GITHUB_PAT }}</span>  
      
    <span class="na">steps</span><span class="pi">:</span>  
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Checkout</span><span class="nv"> </span><span class="s">${{</span><span class="nv"> </span><span class="s">github.ref</span><span class="nv"> </span><span class="s">}}</span><span class="nv"> </span><span class="s">in</span><span class="nv"> </span><span class="s">${{</span><span class="nv"> </span><span class="s">github.repository</span><span class="nv"> </span><span class="s">}}"</span>  
      <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Install</span><span class="nv"> </span><span class="s">dependencies"</span>  
      <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>  
        <span class="s">bundle install</span>
</code></pre></div></div>

<h2 id="cloning-private-repositories-through-swift-package-manager">Cloning Private Repositories through Swift Package Manager</h2>

<p>When Xcode clones dependencies through Swift Package Manager, because it is using the machines git configuration, we can harness that and the <a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-urlltbasegtinsteadOf">insteadOf</a> key to modify the remote URL.</p>

<p>Use the following <code class="language-plaintext highlighter-rouge">step</code> in your GitHub Action to modify the git config remote URL:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Setup</span><span class="nv"> </span><span class="s">Authenticated</span><span class="nv"> </span><span class="s">URL"</span>
  <span class="na">shell</span><span class="pi">:</span> <span class="s">bash</span>
  <span class="na">env</span><span class="pi">:</span>
    <span class="na">GIT_AUTH_TOKEN</span><span class="pi">:</span> <span class="s">${{ secrets.GIT_AUTH_TOKEN }}</span>
  <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">git config --global --add url."https://oauth2:${GIT_AUTH_TOKEN}@github.com/".insteadOf "https://github.com/"</span>
    <span class="s">git config --global user.name "$GIT_AUTHOR_NAME"  </span>
    <span class="s">git config --global user.email "$GIT_AUTHOR_EMAIL"  </span>
    <span class="s">git config --global credential.username "$GIT_USERNAME"</span>
</code></pre></div></div>]]></content><author><name>Tim Searle</name></author><category term="ios" /><category term="ruby" /><category term="github-actions" /><summary type="html"><![CDATA[Generally, GitHub Actions is incredibly simple when interacting with private repositories, either through the GITHUB_TOKEN or through creation a Personal Access Token. However, when the tools that your GitHub Action interacts with require to authenticate, injecting that personal access token can become challenging.]]></summary></entry><entry xml:lang="en"><title type="html">Automatic app version increments with Xcode Cloud using custom build scripts</title><link href="https://www.searle.dev/2022/06/17/xcode-cloud-app-version.html" rel="alternate" type="text/html" title="Automatic app version increments with Xcode Cloud using custom build scripts" /><published>2022-06-17T00:00:00+00:00</published><updated>2022-06-17T00:00:00+00:00</updated><id>https://www.searle.dev/2022/06/17/xcode-cloud-app-version</id><content type="html" xml:base="https://www.searle.dev/2022/06/17/xcode-cloud-app-version.html"><![CDATA[<h3 id="using-swift-to-write-custom-build-scripts-in-xcode-cloud">Using Swift to write custom build scripts in Xcode Cloud</h3>

<p>Recently, as part of my release workflow with the new <a href="https://developer.apple.com/xcode-cloud/">Xcode Cloud</a>, I found myself wanting to ensure that the app version was automatically updated based on the version number specified in my git branch, for example, <code class="language-plaintext highlighter-rouge">release/1.2.0</code>. I was constantly running into the dreaded App Store Connect failure where your app version (<code class="language-plaintext highlighter-rouge">CFBundleShortVersionString</code>) was not greater than the previous release - because I kept forgetting to manually change it in my Info.plist!</p>

<p>To stop making this mistake I wanted to build the following:</p>

<ol>
  <li>Execute my release candidate workflow on Xcode Cloud when I push new commits to a branch matching the pattern <code class="language-plaintext highlighter-rouge">release/*</code></li>
  <li>Within the release candidate workflow, extract the version number from the git branch and update the Info.plist with the specified version</li>
</ol>

<p>In a more concrete scenario - when I wish to generate my next release candidate for TestFlight from my work-in-progress, I can create a new branch with the appropriate version from my changes and then rely solely on Xcode Cloud to execute my app version script and automatically bump the build number.</p>

<p>Xcode Cloud supports <a href="https://developer.apple.com/documentation/xcode/writing-custom-build-scripts">custom build scripts</a>. We can use these scripts to run specific tasks that augment our build pipelines - Xcode Cloud provides 3 entry points:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ci_post_clone</code></li>
  <li><code class="language-plaintext highlighter-rouge">ci_pre_xcodebuild</code></li>
  <li><code class="language-plaintext highlighter-rouge">ci_post_xcodebuild</code></li>
</ul>

<p>For this specific problem, we could use either <code class="language-plaintext highlighter-rouge">ci_post_clone</code> and <code class="language-plaintext highlighter-rouge">ci_pre_xcodebuild</code>, I opted for <code class="language-plaintext highlighter-rouge">ci_post_clone</code> as I want the pipeline to fail as early on as possible if it ran into issues with my script - this reduces build time and therefore cost and reducing the feedback cycle during development.</p>

<p>To get started, we create a directory in the root of our repository named <code class="language-plaintext highlighter-rouge">ci_scripts</code> and create an empty file inside it named <code class="language-plaintext highlighter-rouge">ci_post_clone.sh</code></p>

<p>What language should we use for our script?</p>

<p>By default, Xcode Cloud uses <code class="language-plaintext highlighter-rouge">zsh</code>, but as you’d expect, I am far more comfortable writing Swift. Luckily, we’re able to use Swift within these scripts by specifying the following <a href="https://en.wikipedia.org/wiki/Shebang_(Unix)">shebang</a> at the top of the <code class="language-plaintext highlighter-rouge">ci_post_clone.sh</code> file,</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/usr/bin/env swift
</code></pre></div></div>

<blockquote>
  <p>Note: the only behaviour I have in my <code class="language-plaintext highlighter-rouge">ci_post_clone</code> file is for the app versioning, but it’s likely that you will be handling many scenarios within the 3 available entry point scripts, so it’s important to consider this when structuring your script and the exit points and error points within it - you can read more in the documentation under the “Writing resilient scripts” section.</p>
</blockquote>

<p>Let’s jump into the script bit-by-bit:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">appVersionBumpIfNeeded</span><span class="p">()</span> <span class="k">throws</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">dictionary</span> <span class="o">=</span> <span class="kt">ProcessInfo</span><span class="o">.</span><span class="n">processInfo</span><span class="o">.</span><span class="n">environment</span>
    
    <span class="c1">// ...</span>
</code></pre></div></div>

<p>We start with a simple function signature that throws - this will allow us to exit the script gracefully in the event of an error - and in future handle errors more easily. We also grab a reference to the <code class="language-plaintext highlighter-rouge">ProcessInfo</code> <code class="language-plaintext highlighter-rouge">environment</code> property to access the environment variables on the machine, we’ll be using these to extract information and make conditional decisions.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">guard</span> <span class="k">let</span> <span class="nv">branch</span> <span class="o">=</span> <span class="n">dictionary</span><span class="p">[</span><span class="s">"CI_BRANCH"</span><span class="p">],</span>
          <span class="n">branch</span><span class="o">.</span><span class="nf">starts</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="s">"release/"</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span>
    <span class="p">}</span>
    
    <span class="k">let</span> <span class="nv">version</span> <span class="o">=</span> <span class="n">branch</span><span class="o">.</span><span class="nf">replacingOccurrences</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="s">"release/"</span><span class="p">,</span> <span class="nv">with</span><span class="p">:</span> <span class="s">""</span><span class="p">)</span>
    
    <span class="c1">// ...</span>
</code></pre></div></div>

<blockquote>
  <p>There are a number of Xcode Cloud pre-defined environment variables available to you, these are all prefixed with <code class="language-plaintext highlighter-rouge">CI_</code> - the full reference can be found <a href="https://developer.apple.com/documentation/xcode/environment-variable-reference">here</a></p>
</blockquote>

<p>We extract the branch name, and verify it starts with the prefix (and therefore workflow) we are interested in, if it doesn’t we bail out with no error. We could also take a very thorough approach here and inspect the <code class="language-plaintext highlighter-rouge">CI_WORKFLOW</code> environment variable - but perhaps this is better handled by the caller and the top-level of the script.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">let</span> <span class="nv">version</span> <span class="o">=</span> <span class="n">branch</span><span class="o">.</span><span class="nf">replacingOccurrences</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="s">"release/"</span><span class="p">,</span> <span class="nv">with</span><span class="p">:</span> <span class="s">""</span><span class="p">)</span><span class="o">.</span><span class="nf">replacingOccurrences</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="s">"ci_testing/"</span><span class="p">,</span> <span class="nv">with</span><span class="p">:</span> <span class="s">""</span><span class="p">)</span>
    
    <span class="k">let</span> <span class="nv">components</span> <span class="o">=</span> <span class="n">version</span><span class="o">.</span><span class="nf">components</span><span class="p">(</span><span class="nv">separatedBy</span><span class="p">:</span> <span class="s">"."</span><span class="p">)</span>
    
    <span class="k">guard</span> <span class="n">components</span><span class="o">.</span><span class="n">count</span> <span class="o">&lt;=</span> <span class="mi">3</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"Version invalid length: </span><span class="se">\(</span><span class="n">version</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
        <span class="k">throw</span> <span class="kt">PostCloneErrors</span><span class="o">.</span><span class="n">invalidBranchVersion</span>
    <span class="p">}</span>
    
    <span class="k">let</span> <span class="nv">values</span> <span class="o">=</span> <span class="n">components</span><span class="o">.</span><span class="n">compactMap</span> <span class="p">{</span> <span class="kt">Int</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
    
    <span class="k">guard</span> <span class="n">components</span><span class="o">.</span><span class="n">count</span> <span class="o">==</span> <span class="n">values</span><span class="o">.</span><span class="n">count</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"Version contains invalid characters: </span><span class="se">\(</span><span class="n">version</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
        <span class="k">throw</span> <span class="kt">PostCloneErrors</span><span class="o">.</span><span class="n">invalidBranchVersion</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>The next part is all about validating the version number meets the format expected by <a href="https://help.apple.com/xcode/mac/current/#/devc092854f5">App Store Connect</a>.</p>

<ol>
  <li>Remove the branch name prefix <code class="language-plaintext highlighter-rouge">/release</code></li>
  <li>Extract the individual digits of the version, e.g. <code class="language-plaintext highlighter-rouge">1.2.3</code></li>
  <li>Verify there is at least 1 component and no more than 3</li>
  <li>Verify each component is a valid integer and that the count matches the original number of components</li>
</ol>

<p>If any of the above fails, we want to be throwing an error at this point for the caller to handle or for the script to bail out.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">let</span> <span class="nv">infoPlistURL</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">fileURLWithPath</span><span class="p">:</span> <span class="s">"path/to/Info.plist"</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">infoPlistData</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">Data</span><span class="p">(</span><span class="nv">contentsOf</span><span class="p">:</span> <span class="n">infoPlistURL</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">infoPlist</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">PropertyListSerialization</span><span class="o">.</span><span class="nf">propertyList</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">infoPlistData</span><span class="p">,</span>
                                                               <span class="nv">options</span><span class="p">:</span> <span class="o">.</span><span class="n">mutableContainersAndLeaves</span><span class="p">,</span>
                                                               <span class="nv">format</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> <span class="k">as?</span> <span class="kt">NSDictionary</span>
    <span class="k">let</span> <span class="nv">mutableInfoPlist</span> <span class="o">=</span> <span class="n">infoPlist</span><span class="p">?</span><span class="o">.</span><span class="nf">mutableCopy</span><span class="p">()</span> <span class="k">as?</span> <span class="kt">NSMutableDictionary</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="s">"Updating version to: </span><span class="se">\(</span><span class="n">version</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    
    <span class="n">mutableInfoPlist</span><span class="p">?[</span><span class="s">"CFBundleShortVersionString"</span><span class="p">]</span> <span class="o">=</span> <span class="n">version</span>
    
    <span class="k">let</span> <span class="nv">modifiedInfoPlistData</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">PropertyListSerialization</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span><span class="nv">fromPropertyList</span><span class="p">:</span> <span class="n">mutableInfoPlist</span> <span class="k">as</span> <span class="kt">Any</span><span class="p">,</span> <span class="nv">format</span><span class="p">:</span> <span class="o">.</span><span class="n">xml</span><span class="p">,</span> <span class="nv">options</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
    <span class="k">try</span> <span class="n">modifiedInfoPlistData</span><span class="o">.</span><span class="nf">write</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">infoPlistURL</span><span class="p">)</span>
</code></pre></div></div>

<p>Now that we know the version number matches a valid format, we need to write value that to the <code class="language-plaintext highlighter-rouge">CFBundleShortVersionString</code> key within the Info.plist file in your repository. We do not need to modify the <code class="language-plaintext highlighter-rouge">CFBundleVersion</code> at all, e.g. your build number - as this can be handled in your <a href="https://developer.apple.com/documentation/xcode/setting-the-next-build-number-for-xcode-cloud-builds">Xcode Cloud workflow directly</a>.</p>

<ol>
  <li>Read the contents of Info.plist file to a Data variable</li>
  <li>Deserialize this into a property list and convert it to a NSMutableDictionary so that we can make modifications</li>
  <li>Update the value for the key <code class="language-plaintext highlighter-rouge">CFBundleShortVersionString</code> to the <code class="language-plaintext highlighter-rouge">version</code> we captured earlier</li>
  <li>Serialize the dictionary back into property list data</li>
  <li>Write the modified data back to the same location, overwriting the previous plist</li>
</ol>

<p>And that’s it - I’ll paste the full script below, you’ll need to modify it for your project differences - let me know your thoughts on <a href="https://twitter.com/timsearle_">Twitter</a> and if you’ve made any useful Swift scripts for Xcode Cloud.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#!/usr/bin/env swift</span>
<span class="kd">import</span> <span class="kt">Foundation</span>

<span class="kd">enum</span> <span class="kt">PostCloneErrors</span><span class="p">:</span> <span class="kt">Error</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">invalidBranchVersion</span>
    <span class="k">case</span> <span class="n">invalidInfoPlistPath</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">appVersionBumpIfNeeded</span><span class="p">()</span> <span class="k">throws</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">dictionary</span> <span class="o">=</span> <span class="kt">ProcessInfo</span><span class="o">.</span><span class="n">processInfo</span><span class="o">.</span><span class="n">environment</span>
    
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">branch</span> <span class="o">=</span> <span class="n">dictionary</span><span class="p">[</span><span class="s">"CI_BRANCH"</span><span class="p">],</span>
          <span class="n">branch</span><span class="o">.</span><span class="nf">starts</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="s">"release/"</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span>
    <span class="p">}</span>
    
    <span class="k">let</span> <span class="nv">version</span> <span class="o">=</span> <span class="n">branch</span><span class="o">.</span><span class="nf">replacingOccurrences</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="s">"release/"</span><span class="p">,</span> <span class="nv">with</span><span class="p">:</span> <span class="s">""</span><span class="p">)</span>
    
    <span class="k">let</span> <span class="nv">components</span> <span class="o">=</span> <span class="n">version</span><span class="o">.</span><span class="nf">components</span><span class="p">(</span><span class="nv">separatedBy</span><span class="p">:</span> <span class="s">"."</span><span class="p">)</span>
    
    <span class="k">guard</span> <span class="o">!</span><span class="n">components</span><span class="o">.</span><span class="n">isEmpty</span><span class="p">,</span> <span class="n">components</span><span class="o">.</span><span class="n">count</span> <span class="o">&lt;=</span> <span class="mi">3</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"Version invalid length: </span><span class="se">\(</span><span class="n">version</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
        <span class="k">throw</span> <span class="kt">PostCloneErrors</span><span class="o">.</span><span class="n">invalidBranchVersion</span>
    <span class="p">}</span>
    
    <span class="k">let</span> <span class="nv">values</span> <span class="o">=</span> <span class="n">components</span><span class="o">.</span><span class="n">compactMap</span> <span class="p">{</span> <span class="kt">Int</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
    
    <span class="k">guard</span> <span class="n">components</span><span class="o">.</span><span class="n">count</span> <span class="o">==</span> <span class="n">values</span><span class="o">.</span><span class="n">count</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"Version contains invalid characters: </span><span class="se">\(</span><span class="n">version</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
        <span class="k">throw</span> <span class="kt">PostCloneErrors</span><span class="o">.</span><span class="n">invalidBranchVersion</span>
    <span class="p">}</span>
    
    <span class="k">let</span> <span class="nv">infoPlistURL</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">fileURLWithPath</span><span class="p">:</span> <span class="s">"/your/path/to/Info.plist"</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">infoPlistData</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">Data</span><span class="p">(</span><span class="nv">contentsOf</span><span class="p">:</span> <span class="n">infoPlistURL</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">infoPlist</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">PropertyListSerialization</span><span class="o">.</span><span class="nf">propertyList</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">infoPlistData</span><span class="p">,</span>
                                                               <span class="nv">options</span><span class="p">:</span> <span class="o">.</span><span class="n">mutableContainersAndLeaves</span><span class="p">,</span>
                                                               <span class="nv">format</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> <span class="k">as?</span> <span class="kt">NSDictionary</span>
    <span class="k">let</span> <span class="nv">mutableInfoPlist</span> <span class="o">=</span> <span class="n">infoPlist</span><span class="p">?</span><span class="o">.</span><span class="nf">mutableCopy</span><span class="p">()</span> <span class="k">as?</span> <span class="kt">NSMutableDictionary</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="s">"Updating version to: </span><span class="se">\(</span><span class="n">version</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    
    <span class="n">mutableInfoPlist</span><span class="p">?[</span><span class="s">"CFBundleShortVersionString"</span><span class="p">]</span> <span class="o">=</span> <span class="n">version</span>
    
    <span class="k">let</span> <span class="nv">modifiedInfoPlistData</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">PropertyListSerialization</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span><span class="nv">fromPropertyList</span><span class="p">:</span> <span class="n">mutableInfoPlist</span> <span class="k">as</span> <span class="kt">Any</span><span class="p">,</span> <span class="nv">format</span><span class="p">:</span> <span class="o">.</span><span class="n">xml</span><span class="p">,</span> <span class="nv">options</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
    <span class="k">try</span> <span class="n">modifiedInfoPlistData</span><span class="o">.</span><span class="nf">write</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">infoPlistURL</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">try</span> <span class="nf">appVersionBumpIfNeeded</span><span class="p">()</span>

</code></pre></div></div>

<p>As mentioned earlier - there’s definitely an opportunity at the call-site (<code class="language-plaintext highlighter-rouge">try appVersionBumpIfNeeded()</code>) to inspect the <code class="language-plaintext highlighter-rouge">CI_WORKFLOW</code> name and decide what sequence of functions you wish to call as part of your <code class="language-plaintext highlighter-rouge">ci_post_clone</code> script, e.g.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">Workflow</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">release</span>
<span class="p">}</span>

<span class="k">guard</span> <span class="k">let</span> <span class="nv">workflow</span> <span class="o">=</span> <span class="kt">Workflow</span><span class="p">(</span><span class="nv">rawValue</span><span class="p">:</span> <span class="kt">ProcessInfo</span><span class="o">.</span><span class="n">processInfo</span><span class="o">.</span><span class="n">environment</span><span class="p">[</span><span class="s">"CI_WORKFLOW"</span><span class="p">]</span> <span class="p">??</span> <span class="s">""</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
	<span class="k">return</span>
<span class="p">}</span>

<span class="k">switch</span> <span class="n">workflow</span> <span class="p">{</span>
	<span class="k">case</span> <span class="o">.</span><span class="nv">release</span><span class="p">:</span>
		<span class="k">try</span> <span class="nf">appVersionBumpIfNeeded</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Best of luck!</p>]]></content><author><name>Tim Searle</name></author><category term="ios" /><category term="xcode" /><summary type="html"><![CDATA[Using Swift to write custom build scripts in Xcode Cloud]]></summary></entry><entry xml:lang="en"><title type="html">Adding support for Apple App Site Association files to Jekyll</title><link href="https://www.searle.dev/apple/2022/06/05/aasa-jekyll.html" rel="alternate" type="text/html" title="Adding support for Apple App Site Association files to Jekyll" /><published>2022-06-05T00:00:00+00:00</published><updated>2022-06-05T00:00:00+00:00</updated><id>https://www.searle.dev/apple/2022/06/05/aasa-jekyll</id><content type="html" xml:base="https://www.searle.dev/apple/2022/06/05/aasa-jekyll.html"><![CDATA[<p>It’s pretty common for an iOS app these days to support universal links, iCloud credentials, App Clips and other functionality related to the <a href="https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_associated-domains">Associated Domains</a> entitlement. The tricky part can sometimes be that you <em>don’t</em> have a website or hosting to host the required <code class="language-plaintext highlighter-rouge">apple-app-site-association</code> file for the Apple CDN to pull.</p>

<p>This blog is actually running on a static site generator called Jekyll, and it’s hosted by GitHub Pages. There’s an excellent <a href="https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll">guide</a> from GitHub if you’d like to do something similar - I’m also using it to satisfy the App Store privacy policy requirement too.</p>

<p>For the AASA file, I had two requirements:</p>

<ul>
  <li>Upload a JSON file, named <code class="language-plaintext highlighter-rouge">apple-app-site-association</code> to the <code class="language-plaintext highlighter-rouge">.well-known</code> directory</li>
  <li>Expose this file via my domain <a href="https://searle.dev">searle.dev</a> at that location via Jekyll - this enables the Apple CDN to locate the file, and cache it, based on the domain(s) you specify in your app’s entitlements.</li>
</ul>

<p>It turned out to be <em>really</em> simple, all that was needed was to create the folder and file in my root directory and then the key part - specify in my <code class="language-plaintext highlighter-rouge">_config.yml</code> file to include that location:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">include</span><span class="pi">:</span> 
  <span class="pi">-</span> <span class="s">.well-known</span>
</code></pre></div></div>

<p>There was some misleading information out there that lead me away from this simple approach initially - this works. You can find the commit <a href="https://github.com/timsearle/timsearle.github.io/commit/b04b67860db4d1b080c3ca7bee466d71b03113ea">here</a></p>]]></content><author><name>Tim Searle</name></author><category term="apple" /><category term="ios" /><summary type="html"><![CDATA[It’s pretty common for an iOS app these days to support universal links, iCloud credentials, App Clips and other functionality related to the Associated Domains entitlement. The tricky part can sometimes be that you don’t have a website or hosting to host the required apple-app-site-association file for the Apple CDN to pull.]]></summary></entry></feed>