<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Acter</title>
    <description>The latest articles on DEV Community by Acter (@acter).</description>
    <link>https://dev.to/acter</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F7930%2F29e7b2cb-116e-4445-93b5-5004abb377e9.png</url>
      <title>DEV Community: Acter</title>
      <link>https://dev.to/acter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/acter"/>
    <language>en</language>
    <item>
      <title>Beware of the DashMap deadlock</title>
      <dc:creator>Benjamin Kampmann</dc:creator>
      <pubDate>Fri, 29 Mar 2024 23:00:00 +0000</pubDate>
      <link>https://dev.to/acter/beware-of-the-dashmap-deadlock-lij</link>
      <guid>https://dev.to/acter/beware-of-the-dashmap-deadlock-lij</guid>
      <description>&lt;p&gt;Rust is famously build for the multi-threaded-processor world. From its core ownership-enforcement-model up to the type-based &lt;code&gt;Sync&lt;/code&gt; + &lt;code&gt;Send&lt;/code&gt;-types, all is around allowing the compiler to ensure memory safety and consistency across thread boundaries. And though the &lt;code&gt;std&lt;/code&gt; also has collections (like &lt;code&gt;HashMap&lt;/code&gt; and &lt;code&gt;BTreeSet&lt;/code&gt;), Atomics and Locks, once you start building real programs with Rust, probably some &lt;code&gt;tokio&lt;/code&gt; for async-support as well, these are not always sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  DashMap for the Win
&lt;/h2&gt;

&lt;p&gt;No wonder that you can pick from a handful of libraries helping you achieve this feat, and quite a feat that is, with &lt;code&gt;DashMap&lt;/code&gt; being among the most popular with a whopping 52million downloads on crates.io at the time of this writing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crates.io/crates/dashmap"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zQf-wSac--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/http://www.gnunicorn.org/assets/posts/beware-of-the-dashmap-deadlock-screenshotx15ppalrhabysr821cd7.png" alt="Screenshot of the crates.io entry for dashmap" width="800" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;52million downloads, years since publication, updated just a few months ago. That looks like a reasonably sound library. So, you start playing with it and find that its API is convenient, seems to work across &lt;code&gt;await&lt;/code&gt;s and async and all the things you've ever dreamed of. So you implement it as the main caching layer for the transient state machine of models within the core business logic of your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deadlocks? Really?
&lt;/h2&gt;

&lt;p&gt;I is only a long while later, until the first reports come in. &lt;a href="https://github.com/acterglobal/a3/issues/958"&gt;Sparse at first&lt;/a&gt; and &lt;a href="https://github.com/acterglobal/a3/issues/1264"&gt;unclear in its origin&lt;/a&gt;, but sometimes, it seems, your state machine processing doesn't process the events coming in. Or, better - &lt;a href="https://github.com/acterglobal/a3/pull/1479"&gt;as you learn when digging into it&lt;/a&gt; - &lt;strong&gt;their futures never resolve&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You see, already known &lt;a href="https://github.com/xacrimon/dashmap/issues/243#issuecomment-1368180321"&gt;since at least December 2022&lt;/a&gt; is that you can use &lt;code&gt;DashMap&lt;/code&gt; in a way that can cause deadlocks &lt;em&gt;and without the compiler detecting them&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  A primer on dead locks
&lt;/h3&gt;

&lt;p&gt;If you have no further knowledge about locks and have never heard of deadlocks, let me give you a minimal rough cut of the problem (overly simplified): Sometimes you have memory that is accessed by multiple threads, but clearly if both write at the same time this can cause problems. Thus, the concept of "locks" was created: small pieces around the memory that you need to have hold of first before you can write to that memory. While it is locked no other thread can write to it and thus have to wait for their turn. Ensuring they all write-one-at-a-time and not in between one another.&lt;/p&gt;

&lt;p&gt;Now, how ever long you hold that lock is your prerogative and there are several problems with holding a lock very long. For example: what if your thread panics while you hold the lock? This in rust is usually referred to as a "poisoned lock", you might have seen &lt;a href="https://doc.rust-lang.org/std/sync/struct.PoisonError.html"&gt;that Error in the std&lt;/a&gt;, and how to deal with that depends on the specific code.&lt;/p&gt;

&lt;p&gt;In this case, we are looking into a so called &lt;em&gt;dead-lock situation&lt;/em&gt;. This can even be cause by a single thread easily: when you hold the lock and your code, running on the same thread, for whatever reason, tries to acquire the same lock &lt;em&gt;while still holding the lock&lt;/em&gt;. This stops the execution as the thread is waiting on the lock it itself is holding and thus preventing from being released.&lt;/p&gt;

&lt;p&gt;This type of scenario can be and in the &lt;code&gt;std&lt;/code&gt;-cases is detected by the rust compiler (yay), but not in the case of &lt;code&gt;DashMap&lt;/code&gt;. As DashMap &lt;em&gt;actively&lt;/em&gt; allows for locks to be held over &lt;code&gt;await&lt;/code&gt;-points (that is kinda its jam ... that it allows the user to do that), it isn't possible for the compiler to figure out that this might lead to a dead lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to avoid that problem
&lt;/h2&gt;

&lt;p&gt;The best advice is the one given from [Alice in her post from January 2022] already:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It is especially important to follow the advice about &lt;strong&gt;never locking it in async code&lt;/strong&gt; when using &lt;code&gt;dashmap&lt;/code&gt; for this reason.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While this is good advice, this isn't really mentioned on the docs of &lt;code&gt;DashMap&lt;/code&gt; and considering there is nothing detectable wrong with the &lt;a href="https://github.com/xacrimon/dashmap/issues/243#issuecomment-1370273098"&gt;examples&lt;/a&gt; showing &lt;a href="https://github.com/xacrimon/dashmap/issues/243#issuecomment-1368184568"&gt;the problem&lt;/a&gt; when looking at the code &lt;strong&gt;this is quite the foot gun&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, the &lt;a href="https://github.com/acterglobal/a3/blob/9615edd751103eff5ef09404cb979e9c2c683424/native/core/src/store.rs#L190-L225"&gt;code in question in our case doesn't even hold any locks over &lt;code&gt;await&lt;/code&gt;-points&lt;/a&gt;, yet it seems to deadlock in some race condition scenarios.&lt;/p&gt;

&lt;p&gt;Then you only find out about it after some long debugging and researching the github issues of that dependency. Taking all that into account, and then that there is no real way for you to create tests or otherwise automatically ensure it isn't reintroduced by any further update the code ... I consider this pretty harmful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it?
&lt;/h2&gt;

&lt;p&gt;Well, supposedly, this is fixed the in next big iteration of DashMap, &lt;a href="https://github.com/xacrimon/dashmap/issues/150"&gt;which is said to have async support by getting rid of locks entirely&lt;/a&gt;, but with the issue open since 2021 and most of the ideas of how to avoid the locks being discounted for now, there is no telling &lt;em&gt;when&lt;/em&gt; this come - if ever. What I have seen most people do referencing that issue, and &lt;a href="https://github.com/acterglobal/a3/pull/1479"&gt;what we also ended up doing is&lt;/a&gt;: replace or at least remove DashMap from the code base.&lt;/p&gt;

&lt;p&gt;In our case we replaced it with the up and coming &lt;a href="https://crates.io/crates/scc"&gt;scc&lt;/a&gt;, which uses a different locking concept and has the additional benefit of being faster. Others have opted for &lt;code&gt;cachemap2&lt;/code&gt; or replaced it with the std lock &amp;amp; hashmap: there at least the compiler will tell you if you accidentally created a dead-lock-scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  No disrespect
&lt;/h3&gt;

&lt;p&gt;I am not writing this post to shit on the authors of DashMap, nor its contributors or maintainers. Building a async-safe lock-free-ish collection is a hard task. One that I wouldn't even really want to attempt myself. I still personally don't even understand why this deadlocks internally myself, nor would I consider trying to patch it either - considering that they haven't done it yet makes leads me to believe this isn't an easy thing to do. As such I don't think anyone should be mad about them either, call them names or do any of the other nasty things the internet can do to people that lost its favor.&lt;/p&gt;

&lt;p&gt;I am raising this issue because this is a pretty widely spread library, probably the most popular for the concurrent hashmaps and this is a severe problem that you should know about when using it. That's why I spent a significant amount of this post explaining the core problem and how to avoid it. So, if you use DashMap and want to continue using it, you know what to look out for now.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Edit 2024-04-01&lt;/em&gt;: Edited for clarity, based on &lt;a href="https://lobste.rs/s/xz6daj/beware_dashmap_deadlock"&gt;corresponding feedback&lt;/a&gt; and removed a misleading quote.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>deadlock</category>
      <category>dashmap</category>
      <category>tech</category>
    </item>
    <item>
      <title>Adding a new Ghost via docker-compose to your traefik setup</title>
      <dc:creator>Benjamin Kampmann</dc:creator>
      <pubDate>Fri, 02 Feb 2024 19:31:30 +0000</pubDate>
      <link>https://dev.to/acter/adding-a-new-ghost-via-docker-compose-to-your-traefik-setup-4lc6</link>
      <guid>https://dev.to/acter/adding-a-new-ghost-via-docker-compose-to-your-traefik-setup-4lc6</guid>
      <description>&lt;p&gt;Sometimes the easiest and quickest way to try (or even deploy) a new service is by using the recommended docker-compose-setup that they often have as an example. But if you have an existing infrastructure, like we do with the great &lt;a href="https://github.com/mother-of-all-self-hosting/mash-playbook"&gt;mother of all self-hosting&lt;/a&gt; ansible playbooks, this isn't always easy to integrate. In particular when that infrastructure is managed and started and stopped independently from the additional docker-compose you intend to add. Lucky, who is running their out-most proxy using traefik, because with just a few extra labels your docker-compose becomes available TLS-certs included.&lt;/p&gt;

&lt;p&gt;Fortunately for us the MASH-playbook uses traefik and so adding a Ghost setup for testing was quick and easy. Let's look at the docker-compose (for our fictional &lt;code&gt;blog.example.org&lt;/code&gt;-address) and then we'll explain some of the specific aspects to address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.1'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;ghost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghost:5-alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# see https://ghost.org/docs/config/#configuration-options&lt;/span&gt;
      &lt;span class="na"&gt;database__client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
      &lt;span class="na"&gt;database__connection__host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;database__connection__user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
      &lt;span class="na"&gt;database__connection__password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SOME_PRIVATE_ROOT_PASSWORD&lt;/span&gt;
      &lt;span class="na"&gt;database__connection__database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghost&lt;/span&gt;
      &lt;span class="c1"&gt;# this url value is just an example, and is likely wrong for your environment!&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://blog.example.org&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/ghost:/var/lib/ghost/content&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;aliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;blog-example-org&lt;/span&gt;

    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.enable=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.docker.network=traefik&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.blog-example-org.rule=Host(`blog.example.org`)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.services.blog-example-org.loadbalancer.server.port=2368&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.blog-example-org.entrypoints=web-secure&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.blog-example-org.tls=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.blog-example-org.tls.certResolver=default&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik.http.routers.blog-example-org.service=blog-example-org&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8.0&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SOME_PRIVATE_ROOT_PASSWORD&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghost&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/db:/var/lib/mysql&lt;/span&gt;


&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alright, there's a few things here that have changes compared to the default example from ghost. We will be ignoring the specific Ghost and MySQL changes as they aren't that relevant but are only included for completeness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The networks
&lt;/h2&gt;

&lt;p&gt;First and foremost, we have the additional &lt;code&gt;networks&lt;/code&gt;-section a the bottom of the configuration with two networks: &lt;code&gt;default&lt;/code&gt; which we will use for this specific service and the other that is bridging to the &lt;code&gt;traefik&lt;/code&gt;-service, which is marked as &lt;code&gt;external: true&lt;/code&gt; telling docker to use the existing set up network. This must the the network the dockerized &lt;code&gt;traefik&lt;/code&gt; is using. In the case of MASH this is just called &lt;code&gt;traefik&lt;/code&gt; as well.&lt;/p&gt;

&lt;p&gt;Secondly we need to the &lt;code&gt;networks&lt;/code&gt;-section to both our services, where any internal service is only on the &lt;code&gt;default&lt;/code&gt; network and the exposed service must also be on the &lt;code&gt;traefik&lt;/code&gt;-network. Here we also give it some specific DNS name within that network for traefik to route the traffic to.&lt;/p&gt;

&lt;h2&gt;
  
  
  the &lt;code&gt;traefik&lt;/code&gt; labels
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;traefik&lt;/code&gt; is set up to use docker-labels, which is the case in our MASH setup, we can just label our service with a view fields and the &lt;code&gt;traefik&lt;/code&gt; service will automatically recognize and configure the routing appropriately. Let's go through them one by one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;traefik.enable=true&lt;/code&gt;: to configure traefik to route this one. Depending on your setup this might not be needed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;traefik.docker.network=traefik&lt;/code&gt;: the network traefik is on&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;traefik.http.routers.blog-example-org.rule=Host(\&lt;/code&gt;blog.example.org&lt;code&gt;)&lt;/code&gt;: the actual hostname we want this service to be available under between the final ticks. Note that we are creating a custom traefik-&lt;code&gt;router&lt;/code&gt; for this called &lt;code&gt;blog-example-org&lt;/code&gt;, all the following configuration is also using that router prefix:&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;traefik.http.services.blog-example-org.loadbalancer.server.port=2368&lt;/code&gt;: the port on this service the traffic should be routed to&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;traefik.http.routers.blog-example-org.entrypoints=web-secure&lt;/code&gt;: if the traefik has multiple outsid eendpoints, which ones to serve - in the MASH case we want this to be available at &lt;code&gt;https&lt;/code&gt;, which is named &lt;code&gt;web-secure&lt;/code&gt; in our setup.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;traefik.http.routers.blog-example-org.tls=true&lt;/code&gt;: to enable TLS for this router&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;traefik.http.routers.blog-example-org.tls.certResolver=default&lt;/code&gt;: use the default DNS cert resolving functionality. In MASH this means we are using lets-encrypt certification&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;traefik.http.routers.blog-example-org.service=blog-example-org&lt;/code&gt;: the service to route the traffic to. The value is the dns-alias we gave in the network configuration before.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  up and go
&lt;/h2&gt;

&lt;p&gt;And that's about it. Assuming the DNS name already resolves to your server and your traefik is already running just doing a &lt;code&gt;docker compose up -d&lt;/code&gt; and a short time later (if it needs to fetch the certificates for the first time), the service will be routed through and be available at &lt;code&gt;blog.example.org&lt;/code&gt;. Neat!&lt;/p&gt;

</description>
      <category>traefik</category>
      <category>docker</category>
      <category>ghost</category>
      <category>devops</category>
    </item>
    <item>
      <title>Six niche tips for shipping Flutter MacOS builds</title>
      <dc:creator>Benjamin Kampmann</dc:creator>
      <pubDate>Thu, 11 Jan 2024 11:00:00 +0000</pubDate>
      <link>https://dev.to/acter/six-niche-tips-for-shipping-flutter-macos-builds-10cg</link>
      <guid>https://dev.to/acter/six-niche-tips-for-shipping-flutter-macos-builds-10cg</guid>
      <description>&lt;p&gt;Ever since we started shipping &lt;a href="https://next.acter.global"&gt;Acter&lt;/a&gt; to the Apple iOS AppStore, we wanted to have it on the Apple MacOS Store as well. With us building it on Rust and Flutter this should have been quite an easy feat as both have native support for MacOS. Yet actually shipping it was multiple months of try and error—with the last month spent on just a tiny problem caused by the Github Actions Runners. these are six niche tips we wished someone had told us before, that would have saved us months of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nightly builds as the baseline
&lt;/h2&gt;

&lt;p&gt;For testing and internal distribution we had nightly builds in Acter for a few months already. They would automatically be created &lt;a href="https://github.com/acterglobal/a3/blob/e540ef02a640b90b5880b126f89d13a59d7fb409/.github/workflows/nightly.yml#L30-L31"&gt;every night at 3am&lt;/a&gt; (hence the name) &lt;a href="https://github.com/acterglobal/a3/blob/e540ef02a640b90b5880b126f89d13a59d7fb409/.github/workflows/nightly.yml#L34-L52"&gt;if changes had been found on the &lt;code&gt;main&lt;/code&gt; branch&lt;/a&gt;. Our build here consists of two parts: the internal Rust library and then we follow with a simple &lt;code&gt;flutter build $target&lt;/code&gt;. So obviously, we have &lt;a href="https://github.com/acterglobal/a3/blob/e540ef02a640b90b5880b126f89d13a59d7fb409/.github/workflows/nightly.yml#L70-L124"&gt;created a nice Github Actions Matrix&lt;/a&gt; to reuse as much as possible. I am not going into too much detail here and the latest action setup probably already changed when you read this, but I have linked the specific sections for record. the&lt;/p&gt;

&lt;h2&gt;
  
  
  1. MacOS is iOS but different—and Google won’t tell you
&lt;/h2&gt;

&lt;p&gt;For the release build of &lt;em&gt;iOS&lt;/em&gt; to work, we needed to sign the app. As a matter of fact, the flutter build won’t really work if you don’t have the necessary signatures set up. For iOS nightly we use an Ad-Hoc setup with a few pre-configured internal devices, for the release we used the distribution profiles, both stored as &lt;a href="https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions"&gt;environment secrets&lt;/a&gt; as is so commonly used in many tutorials. For MacOS signatures aren’t necessary to build and distribute the App - presumably because MacOS App development pre-dates signed builds. Thus our nightly builds didn’t have any setup for that yet.&lt;/p&gt;

&lt;p&gt;Another important difference to note is on the flutter side. While flutters &lt;code&gt;build ipa&lt;/code&gt; offer the options &lt;code&gt;--export-options-plist=PATH&lt;/code&gt; allowing you to specify certain plist information overrides &lt;em&gt;for that specific build&lt;/em&gt;, no such option exists in &lt;code&gt;flutter build macos&lt;/code&gt;. Meaning that all the configuration setup inside the macos-folder is and must be used as-is. That is a bit annoying as it means we can’t easily make a local release build without the signatures now but that’s what it is.&lt;/p&gt;

&lt;p&gt;One annoying side-effect of Flutter being a lot more popular for building mobile apps is that when you try to Google for information regarding the apple setup needed you’ll almost exclusively find questions and problems for iOS. They then recommend stuff like the &lt;code&gt;export-options&lt;/code&gt;-command or other obscure settings you are supposed to change via xcode but that doesn’t actually do anything in the desktop version or doesn’t even exist. Google really doesn’t help you when you get stuck with your Flutter MacOS build.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Switching from github environment secrets to git-crypt for signatures and profiles
&lt;/h2&gt;

&lt;p&gt;One thing we wished we had done earlier was switching from storing signatures and provisioning profiles in Github secret environment variables to using &lt;code&gt;git-crypt&lt;/code&gt;`. Many tutorials and setups out there recommend using the Github secrets to store, well, secret information like the provisioning profiles and the secrets from the keychain and then have some companion script that puts that into the local Github Action build. That is all good and dandy if you only do that setup once and rarely change it. But I always found it kinda annoying that despite no hint in the Git history a build might fail or pass. Once you go beyond just managing a single profile the scripts are then often falling apart and the increasing number of environment variables becomes very confusing and it is super easy to mess up in converting them into the right base64 because it was soo long ago you did it last.&lt;/p&gt;

&lt;p&gt;Rather than storing profiles and the keystore and similar file-based secrets in the secret environment variables we switched to using &lt;a href="https://www.agwa.name/projects/git-crypt/"&gt;&lt;code&gt;git-crypt&lt;/code&gt;&lt;/a&gt; a git extension you can configure that transparently encrypts a subset of files before committing them to the repo. That makes it super easy and simple to update them and still keep the files available. Rather than extracting each secret from the environment into a file we just install git-crypt and have the main password as the action secret that we then use to decrypt the files:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Unlock git-crypt&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.with_apple_cert&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;brew install git-crypt&lt;/span&gt;
    &lt;span class="s"&gt;echo "$" | base64 --decode &amp;gt; .github/assets/git-crypt-key&lt;/span&gt;
    &lt;span class="s"&gt;git-crypt unlock .github/assets/git-crypt-key&lt;/span&gt;
    &lt;span class="s"&gt;echo "Files found:"&lt;/span&gt;
    &lt;span class="s"&gt;git-crypt status -e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once we have the files decrypted, we use the commonly used script to import the keychain from the now decrypted file. Technically we wouldn’t even need the extra password for that file, but it also doesn’t hurt:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Install the Apple certificate and provisioning profile&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install the Apple certificates&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.with_apple_cert&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;P12_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.BUILD_CERTS_P12_PASSWORD }}&lt;/span&gt;
    &lt;span class="na"&gt;KEYCHAIN_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KEYCHAIN_PASSWORD }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;echo "starting in $RUNNER_TEMP"&lt;/span&gt;
    &lt;span class="s"&gt;# create variables&lt;/span&gt;
    &lt;span class="s"&gt;CERTIFICATE_PATH=".github/assets/build_certificates.p12"&lt;/span&gt;
    &lt;span class="s"&gt;KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"&lt;/span&gt;
    &lt;span class="s"&gt;echo "vars set"&lt;/span&gt;
    &lt;span class="s"&gt;# import certificate and provisioning profile from secrets&lt;/span&gt;
    &lt;span class="s"&gt;# create temporary keychain&lt;/span&gt;
    &lt;span class="s"&gt;echo "creating keychain"&lt;/span&gt;
    &lt;span class="s"&gt;security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;echo "setting keychain"&lt;/span&gt;
    &lt;span class="s"&gt;security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;echo "unlocking keychain"&lt;/span&gt;
    &lt;span class="s"&gt;security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;# import certificate to keychain&lt;/span&gt;
    &lt;span class="s"&gt;echo "importing certificate"&lt;/span&gt;
    &lt;span class="s"&gt;security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;echo "listing keychains"&lt;/span&gt;
    &lt;span class="s"&gt;security list-keychain -d user -s "$KEYCHAIN_PATH"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And finally we just take all the now decrypted provisioning_profiles files and copy them where they need to be. All files for all builds in the git repo. Sweet.&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Install the Apple certificate and provisioning profile&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install the Apple certificates&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.with_apple_cert&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;P12_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.BUILD_CERTS_P12_PASSWORD }}&lt;/span&gt;
    &lt;span class="na"&gt;KEYCHAIN_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KEYCHAIN_PASSWORD }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;echo "starting in $RUNNER_TEMP"&lt;/span&gt;
    &lt;span class="s"&gt;# create variables&lt;/span&gt;
    &lt;span class="s"&gt;CERTIFICATE_PATH=".github/assets/build_certificates.p12"&lt;/span&gt;
    &lt;span class="s"&gt;KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"&lt;/span&gt;
    &lt;span class="s"&gt;echo "vars set"&lt;/span&gt;
    &lt;span class="s"&gt;# import certificate and provisioning profile from secrets&lt;/span&gt;
    &lt;span class="s"&gt;# create temporary keychain&lt;/span&gt;
    &lt;span class="s"&gt;echo "creating keychain"&lt;/span&gt;
    &lt;span class="s"&gt;security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;echo "setting keychain"&lt;/span&gt;
    &lt;span class="s"&gt;security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;echo "unlocking keychain"&lt;/span&gt;
    &lt;span class="s"&gt;security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;# import certificate to keychain&lt;/span&gt;
    &lt;span class="s"&gt;echo "importing certificate"&lt;/span&gt;
    &lt;span class="s"&gt;security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"&lt;/span&gt;
    &lt;span class="s"&gt;echo "listing keychains"&lt;/span&gt;
    &lt;span class="s"&gt;security list-keychain -d user -s "$KEYCHAIN_PATH"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally don’t forget to clean all that up, regardless of whether the build failed or succeeded!&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Clean up keychain and provisioning profile&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ always() }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;security delete-keychain $RUNNER_TEMP/app-signing.keychain-db&lt;/span&gt;
    &lt;span class="s"&gt;rm ~/Library/MobileDevice/Provisioning\ Profiles/*&lt;/span&gt;
    &lt;span class="s"&gt;rm .github/assets/git-crypt-key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This makes it super easy to update all that data. Got a new provisioning profile? Just put it into that folder. Update to the keystore? Just export the p12-file with the same password again. No base64 conversion, no copying into the Github Secrets - just &lt;code&gt;git commit &amp;amp;&amp;amp; push&lt;/code&gt;. &lt;em&gt;*chefskiss&lt;/em&gt;*.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Github Search is the hidden champion
&lt;/h2&gt;

&lt;p&gt;One of Githubs most underrated features is its search. It being the biggest crowd source code knowledge base in the world, including the largest source for all their own configuration files (which the workflow-yamls are one of) their search can be truly amazing. Not everyone who found the hack to make something happen will blog about it or write a stack overflow—this post almost didn’t make it either. But it if works there is a high chance they commit it and it ends up in the Github repo, discoverable via the search.&lt;/p&gt;

&lt;p&gt;Similar as Google, Github’s search has &lt;a href="https://github.com/search/advanced"&gt;many advanced options&lt;/a&gt;. For us looking for alternative ways of doing the Flutter build within the Actions, adding the &lt;a href="https://github.com/search?q=path%3A.github%2Fworkflow+flutter+macos&amp;amp;type=code"&gt;&lt;code&gt;path:.github/workflow flutter macos&lt;/code&gt;&lt;/a&gt; was the key to unlocking a treasure of knowledge. Mind you that even though code is committed doesn’t necessarily mean it runs, though. But it is how we first found out about the git-crypt idea! And that’s also how we found out about the final upload pattern we ended up using.&lt;/p&gt;

&lt;p&gt;Seriously, if you are ever stuck on some Github Action configuration that others probably already attempted try the Github search. Google doesn’t even know about a fraction of it and with the advanced search you can make the queries very specific to your problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. MacOS apps need all their binaries signed
&lt;/h2&gt;

&lt;p&gt;One particularly nasty difference between the iOS and MacOS flutter build is that the latter doesn’t really manage the signing properly for you. Singing the Mac App is different than the iOS one, too: While on iOS you create an &lt;code&gt;ipa&lt;/code&gt;-file (effectively a Zip-File) which is then signed as a whole (oversimplified), the “Mac App” is actually a directory with the extension &lt;code&gt;.app&lt;/code&gt;. You can’t effectively “sign” directories. Instead the people at Apple decided that what you must do is sign each binary within that app directory and provide these signatures in the directory. This is hidden in the docs somewhere but if you tried to Google for this information, you will only find iOS fixes (see No 1). So I am telling you know.&lt;/p&gt;

&lt;p&gt;For most cases that is fairly irrelevant but as we had a bunch of binaries, our own included. We found &lt;a href="https://github.com/acterglobal/a3/commit/6263d5990921ea104daba0abacab0c48dda9e135"&gt;a script that iterates through the final app and signs each binary with the provided credentials&lt;/a&gt;, which we then added to the regular Xcode shell-script build process for release builds. That means that at the end of &lt;code&gt;flutter build macos&lt;/code&gt;, we now have an &lt;code&gt;Acter.app&lt;/code&gt; directory with all the proper signatures included. Yay.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Build code ages quickly
&lt;/h2&gt;

&lt;p&gt;One problem you’ll be facing with the Github Search as well as the Google search answers regardless is that the infrastructure you are building with and against constantly changes. For us, there were several tutorials out there recommending ways of packaging or uploading the app that were outdated to simply not supported anymore for the latest version (this was even worse for building the Windows App). Trying to figure out which is the latest recommended and thus hopefully the longest-lasting code you could write is a tedious and annoying process. Very often you don’t know this isn’t supported anymore until you installed and tried the command. But there is a few tricks to keep in mind, when you find a novel approach you might want to try out: you can check the official docs and see if it is still supported, if it is on Github you can see when it was last run, for StackOverflow and many blogs you can quickly gather whether this is a new or rather old idea. Unfortunately in this space, old often means less likely too still work…&lt;/p&gt;

&lt;p&gt;For us the latest—at the time of writing—and recommended way to package and submit the Flutter MacOS app to the Apple Mac Store is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;build the release version of the app with &lt;code&gt;flutter build macos&lt;/code&gt;; make sure all binaries are signed (see above)&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;productbuild&lt;/code&gt; to create a modern &lt;code&gt;.pkg&lt;/code&gt; and have it signed: &lt;code&gt;productbuild --component Acter.app /Applications --sign "$APPLE_SIGN_CERTNAME" Acter.pkg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;then use &lt;code&gt;altool&lt;/code&gt; to upload the &lt;code&gt;.pkg&lt;/code&gt; to the Apple Mac AppStore using a private_key credential (which we stored with git-crypt, of course): &lt;/li&gt;
&lt;/ol&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload to App Store&lt;/span&gt;
     &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;APPLE_API_KEY_BASE64&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_API_KEY_BASE64 }}&lt;/span&gt;
       &lt;span class="na"&gt;APPLE_API_KEY_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_API_KEY_ID }}&lt;/span&gt;
       &lt;span class="na"&gt;APPLE_ISSUER_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_ISSUER_ID }}&lt;/span&gt;
     &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
       &lt;span class="s"&gt;mkdir private_keys&lt;/span&gt;
       &lt;span class="s"&gt;echo -n "$APPLE_API_KEY_BASE64" | base64 --decode --output "private_keys/AuthKey_$APPLE_API_KEY_ID.p8"&lt;/span&gt;
       &lt;span class="s"&gt;ls -ltas private_keys&lt;/span&gt;
       &lt;span class="s"&gt;xcrun altool --upload-app --type macos --file acter-macosx-${{ needs.tags.outputs.tag }}.pkg \&lt;/span&gt;
           &lt;span class="s"&gt;--bundle-id global.acter.a3 \&lt;/span&gt;
           &lt;span class="s"&gt;--apiKey "$APPLE_API_KEY_ID" \&lt;/span&gt;
           &lt;span class="s"&gt;--apiIssuer "$APPLE_ISSUER_ID"&lt;/span&gt;
     &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;    

&lt;h2&gt;
  
  
  6. Github artifacts are not a proper package mechanism: measure twice
&lt;/h2&gt;

&lt;p&gt;With that we are all set and everything should work. Yet Apple kept rejecting our app. But only after the upload in the post-processing on the server side, a few hours later we’d receive an email saying something along the lines of:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload to App Store&lt;/span&gt;
     &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;APPLE_API_KEY_BASE64&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_API_KEY_BASE64 }}&lt;/span&gt;
       &lt;span class="na"&gt;APPLE_API_KEY_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_API_KEY_ID }}&lt;/span&gt;
       &lt;span class="na"&gt;APPLE_ISSUER_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_ISSUER_ID }}&lt;/span&gt;
     &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
       &lt;span class="s"&gt;mkdir private_keys&lt;/span&gt;
       &lt;span class="s"&gt;echo -n "$APPLE_API_KEY_BASE64" | base64 --decode --output "private_keys/AuthKey_$APPLE_API_KEY_ID.p8"&lt;/span&gt;
       &lt;span class="s"&gt;ls -ltas private_keys&lt;/span&gt;
       &lt;span class="s"&gt;xcrun altool --upload-app --type macos --file acter-macosx-${{ needs.tags.outputs.tag }}.pkg \&lt;/span&gt;
           &lt;span class="s"&gt;--bundle-id global.acter.a3 \&lt;/span&gt;
           &lt;span class="s"&gt;--apiKey "$APPLE_API_KEY_ID" \&lt;/span&gt;
           &lt;span class="s"&gt;--apiIssuer "$APPLE_ISSUER_ID"&lt;/span&gt;
     &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;    

&lt;p&gt;The attentive reader might have already noticed the actual problem already. As we had gotten several similar looking emails (especially the top part) from before when the signatures were not properly set up for each binary, we assumed it was something wrong with that part again. Weirdly enough, when doing the entire process manually (rather than via Github Action) it all worked fine and Apple didn’t reject our submission. Weird. So we downloaded the latest &lt;code&gt;Acter.app&lt;/code&gt; from the build artifacts to try to see if we could sign and submit that. The download was larger than usual (220mb rather than the usual 140mb we saw for most builds before) but we didn’t really think much about it. Indeed, trying to package and upload this version Apple rejected it again.&lt;/p&gt;

&lt;p&gt;So, we look into the insides of the &lt;code&gt;Acter.app&lt;/code&gt; build by the Github Action: it is just a folder after all (even though MacOS finder hides it under the &lt;code&gt;right click -&amp;gt; Open Contents&lt;/code&gt;). Right away we noticed something odd: all binaries for the frameworks appeared to be in there &lt;em&gt;twice&lt;/em&gt;: once under &lt;code&gt;$framework/Versions/Current&lt;/code&gt; and once as &lt;code&gt;$framework/Versions/A&lt;/code&gt;. That sure explained why it would be about twice the size. Interestingly our nightly builds didn’t show this behavior: there &lt;code&gt;Current&lt;/code&gt; was a symlink to &lt;code&gt;A&lt;/code&gt; for each as—we’d expect it to be. So although the nightly build system was the baseline we started with, we must have altered something along the way.&lt;/p&gt;

&lt;p&gt;Then it hit us: &lt;strong&gt;the main difference is that in the nightly job packages the &lt;code&gt;.app&lt;/code&gt;-Folder as a &lt;code&gt;tar.bz&lt;/code&gt; directly and submits it to the Github release from the build job, while in the publishing action we store the folder as a Github Artifact that a second job after downloads and submits to the store&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why does that matter? The Github Artefact is stored as a ZIP, too (to save disk space), after all. Yes, but for zipping by default is that symbolic links &lt;em&gt;are resolved&lt;/em&gt;. As we are zipping the entire folder the symlinks that is usually &lt;code&gt;Versions/Current -&amp;gt; Versions/A&lt;/code&gt; is &lt;em&gt;resolved&lt;/em&gt;, meaning the files are stored twice. Yet the signature is only stored once and only for &lt;code&gt;Versions/A&lt;/code&gt; (not for &lt;code&gt;Versions/Current&lt;/code&gt;). So when we download that zipped version, we have an &lt;code&gt;.app&lt;/code&gt;-folder with each framework version stored twice yet only a signature for one (and the file having about twice the size). Looking at the error messages sent by Apple, the last batch of errors even gives a hint to that problem.&lt;/p&gt;

&lt;p&gt;Finding that issue, one and off, took us a month. Yet the fix was small and trivial: we moved the &lt;code&gt;productbuild&lt;/code&gt; to create a &lt;code&gt;.pkg&lt;/code&gt;-file from the publishing job into the build-job and store that &lt;code&gt;.pkg&lt;/code&gt;-&lt;em&gt;file&lt;/em&gt; as the artefact. Problem solved.&lt;/p&gt;




&lt;p&gt;These are just a few things we wished we had known before. Do you have any additional tips for that—apparently niche—Flutter MacOS build systems you wished someone had told you before? Let us know below!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>acter</category>
      <category>deploy</category>
      <category>githubactions</category>
    </item>
  </channel>
</rss>
