Build log

How do you keep a GitHub outage from breaking your iOS build?

Make pod install work from a local cache first, remove the sources you do not need and retry network failures with a back-off instead of failing on the first error. On 8 November 2026 GitHub release downloads returned 503 for about 30 minutes and took two TestFlight builds of GoTasty, GOGOGO LLC's Unity game, down with them; this is the fix.

6 min read

Black and white image of a dark server room aisle where one monitor shows an HTTP 503 error and a second shows a build log turning green after a retry, with a hooded figure watching from a distance.

On 8 November 2026, GitHub's release downloads answered HTTP 503 for about half an hour. Two iOS builds of GoTasty, our Unity game at GOGOGO LLC, died in the same step: pod install, while fetching UnityAds.zip for the Unity Ads mediation adapter. Nothing in our code had changed. The builds failed because a dependency download needed github.com to be up at that exact minute.

If your iOS project uses CocoaPods, you probably have the same exposure. Many pods are a podspec on the CocoaPods CDN pointing at a zip in a GitHub release. Here is what we changed.

Why does pod install depend on GitHub at all?

Two reasons. First, a pod's source is often a GitHub release asset, so downloading the pod means downloading from GitHub. Second, some tools add the old git specs repository, https://github.com/CocoaPods/Specs, as a source next to the CDN. In our case the Google Mobile Ads Unity plugin writes that line into the generated Podfile. Every pod we use is on the CDN, so the git source added nothing but one more github.com fetch and a stream of "Found multiple specifications" warnings.

Step 1: remove the sources you do not need

Unity's External Dependency Manager generates the Podfile on every build, so editing it by hand does not stick. We delete the git specs source in the build script, right before pod install:

sed -i '' "\#^source 'https://github.com/CocoaPods/Specs'#d" Podfile
grep -n '^source' Podfile || true

The grep prints the remaining sources into the log, so you can see what the build actually used.

Step 2: install from the cache first

CocoaPods keeps downloaded pods in ~/Library/Caches/CocoaPods and specs under ~/.cocoapods. If both are warm and your pods have not changed, pod install needs no network at all. So the first attempt is a plain pod install, without --repo-update. Only if it fails, typically because a spec is missing, do the later attempts update the repo.

Where the cache lives depends on the runner. On our self-hosted Mac mini it simply persists between runs. On a hosted macOS runner it needs actions/cache. Our archive job has no checkout, so there is no lockfile to hash: we restore the newest cache by prefix and save a fresh one per run.

- name: CocoaPods cache (cloud)
  if: ${{ inputs.runner == 'cloud' }}
  uses: actions/cache@v4
  with:
    path: |
      ~/Library/Caches/CocoaPods
      ~/.cocoapods/repos/trunk
    key: pods-${{ runner.os }}-${{ github.run_id }}
    restore-keys: pods-${{ runner.os }}-

Step 3: retry with a back-off

Outages like this one are usually short. A build that fails on the first 503 throws away the Unity export that came before it to save one minute of waiting. Our script tries four times: the plain install, then three attempts with --repo-update, waiting 60 seconds and then 180 seconds in between.

attempts=("" "--repo-update" "--repo-update" "--repo-update")
waits=(0 60 180 0)
for i in "${!attempts[@]}"; do
  echo "::group::pod install ${attempts[$i]} (attempt $((i + 1))/${#attempts[@]})"
  if pod install ${attempts[$i]}; then echo "::endgroup::"; exit 0; fi
  echo "::endgroup::"
  if [ "$i" -lt $((${#attempts[@]} - 1)) ] && [ "${waits[$i]}" -gt 0 ]; then
    echo "pod install failed; next attempt in ${waits[$i]} s"
    sleep "${waits[$i]}"
  fi
done
echo "::error::pod install failed after ${#attempts[@]} attempts"
exit 1

Each attempt gets its own collapsible log group, so a retry is visible in the run without burying the output. If all four fail, the error says so in one line.

How do you test it without waiting for an outage?

You do not have to wait for GitHub to fail. Three checks cover it:

  • Warm cache, no network. On a machine that has built the project before, block github.com (a hosts-file entry on a test machine is enough) and run the install script. With unchanged pods it should succeed on the first attempt.
  • A missing spec. Add a pod version you have never fetched and keep github.com blocked. The script should go through all four attempts, wait between them and end with the one-line error instead of hanging.
  • Normal network. Remove the block and run it again. The plain install should still be the attempt that succeeds, which proves the cache path is the one your builds really use.

Then keep reading the log groups. If a build with unchanged pods ever reaches the second attempt, the cache is not where you think it is, and the next outage will find that out for you.

What does this not fix?

  • A pod you have never downloaded still needs the network. A new dependency added during an outage will still fail, just later and more politely.
  • A cold hosted runner with no restored cache is as exposed as before.
  • Swift packages take a separate path. Xcode resolves them itself, so pin and cache them separately if you use them.

What it does is turn a short GitHub outage from a failed build into a slower one for every build whose dependencies have not changed, which is most of them.

Should you commit your pods instead?

Committing the Pods folder takes the network out of the build entirely, and some teams do it. In a Unity project, where the Xcode project and the Podfile are regenerated on every build, it adds a large, noisy directory to review. A warm cache plus retries gave us most of the benefit with none of the repository weight. The machine that keeps that cache warm is described in Unity CI on a self-hosted Mac mini.

Sources

  1. Releases · Unity-Technologies/unity-ads-ios · GitHub
  2. CocoaPods 1.8 Beta is Here! · CocoaPods Blog
  3. Podfile Syntax Reference · CocoaPods Guides
  4. Command-line Reference · CocoaPods Guides
  5. googlesamples/unity-jar-resolver (External Dependency Manager for Unity) · GitHub
  6. googleads/googleads-mobile-unity · GitHub
  7. Dependency caching reference · GitHub Docs
  8. actions/cache · GitHub

Questions

Asked about this post.

Why does pod install fail when GitHub is down?
Many pods download their source from a GitHub release, and some Podfiles also list the git specs repository on github.com. When GitHub answers with errors such as HTTP 503, those downloads fail and so does pod install.
How do you cache CocoaPods in GitHub Actions?
Cache ~/Library/Caches/CocoaPods and ~/.cocoapods/repos/trunk with actions/cache. On a self-hosted Mac those folders persist between runs on their own. Run pod install without --repo-update first, so a warm cache needs no network.
Should pod install use --repo-update in CI?
Not on the first attempt. Try a plain pod install from the cache, and use --repo-update only in a retry, when a spec is missing.
Does a CocoaPods cache protect against every outage?
No. A pod that was never downloaded still needs the network, a cold runner without a restored cache is still exposed, and Swift packages resolve separately.